mirror of https://github.com/fluxcd/flux2.git
				
				
				
			[RFC-007] Flux cmd support for GitHub provider: This commit includes the following changes -
- Add flux create secret githubapp command that accepts and validates the inputs to create a github app secret with options to export the secret yaml or create the secret directly in the Kubernetes cluster - Add tests for flux create secret githubapp command - Add flux create source git command that accepts and validates the inputs to create a gitrepository source with for github provider with options to export the source yaml or create the github gitrepository source directly in the Kubernetes cluster. - Add tests for flux create source git command for github provider. Signed-off-by: Dipti Pai <diptipai89@outlook.com>pull/5103/head
							parent
							
								
									8bedcc46d4
								
							
						
					
					
						commit
						c15eb30b0d
					
				| @ -0,0 +1,128 @@ | ||||
| /* | ||||
| Copyright 2024 The Flux authors | ||||
| 
 | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
| 
 | ||||
|     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| 
 | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| package main | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 
 | ||||
| 	"github.com/fluxcd/flux2/v2/internal/utils" | ||||
| 	"github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret" | ||||
| 	"github.com/spf13/cobra" | ||||
| 	corev1 "k8s.io/api/core/v1" | ||||
| 	"sigs.k8s.io/yaml" | ||||
| ) | ||||
| 
 | ||||
| var createSecretGitHubAppCmd = &cobra.Command{ | ||||
| 	Use:   "githubapp [name]", | ||||
| 	Short: "Create or update a github app secret", | ||||
| 	Long:  withPreviewNote(`The create secret githubapp command generates a Kubernetes secret that can be used for GitRepository authentication with github app`), | ||||
| 	Example: `  # Create a githubapp authentication secret on disk and encrypt it with Mozilla SOPS | ||||
|   flux create secret githubapp podinfo-auth \ | ||||
|     --app-id="1" \ | ||||
|     --app-installation-id="2" \ | ||||
|     --app-private-key=./private-key-file.pem \ | ||||
|     --export > githubapp-auth.yaml | ||||
| 
 | ||||
|   sops --encrypt --encrypted-regex '^(data|stringData)$' \ | ||||
|     --in-place githubapp-auth.yaml | ||||
| 	`, | ||||
| 	RunE: createSecretGitHubAppCmdRun, | ||||
| } | ||||
| 
 | ||||
| type secretGitHubAppFlags struct { | ||||
| 	appID             string | ||||
| 	appInstallationID string | ||||
| 	privateKeyFile    string | ||||
| 	baseURL           string | ||||
| } | ||||
| 
 | ||||
| var secretGitHubAppArgs = secretGitHubAppFlags{} | ||||
| 
 | ||||
| func init() { | ||||
| 	createSecretGitHubAppCmd.Flags().StringVar(&secretGitHubAppArgs.appID, "app-id", "", "github app ID") | ||||
| 	createSecretGitHubAppCmd.Flags().StringVar(&secretGitHubAppArgs.appInstallationID, "app-installation-id", "", "github app installation ID") | ||||
| 	createSecretGitHubAppCmd.Flags().StringVar(&secretGitHubAppArgs.privateKeyFile, "app-private-key", "", "github app private key file path") | ||||
| 	createSecretGitHubAppCmd.Flags().StringVar(&secretGitHubAppArgs.baseURL, "app-base-url", "", "github app base URL") | ||||
| 
 | ||||
| 	createSecretCmd.AddCommand(createSecretGitHubAppCmd) | ||||
| } | ||||
| 
 | ||||
| func createSecretGitHubAppCmdRun(cmd *cobra.Command, args []string) error { | ||||
| 	if len(args) < 1 { | ||||
| 		return fmt.Errorf("name is required") | ||||
| 	} | ||||
| 
 | ||||
| 	secretName := args[0] | ||||
| 
 | ||||
| 	if secretGitHubAppArgs.appID == "" { | ||||
| 		return fmt.Errorf("--app-id is required") | ||||
| 	} | ||||
| 
 | ||||
| 	if secretGitHubAppArgs.appInstallationID == "" { | ||||
| 		return fmt.Errorf("--app-installation-id is required") | ||||
| 	} | ||||
| 
 | ||||
| 	if secretGitHubAppArgs.privateKeyFile == "" { | ||||
| 		return fmt.Errorf("--app-private-key is required") | ||||
| 	} | ||||
| 
 | ||||
| 	privateKey, err := os.ReadFile(secretGitHubAppArgs.privateKeyFile) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("unable to read private key file: %w", err) | ||||
| 	} | ||||
| 
 | ||||
| 	opts := sourcesecret.Options{ | ||||
| 		Name:                    secretName, | ||||
| 		Namespace:               *kubeconfigArgs.Namespace, | ||||
| 		GitHubAppID:             secretGitHubAppArgs.appID, | ||||
| 		GitHubAppInstallationID: secretGitHubAppArgs.appInstallationID, | ||||
| 		GitHubAppPrivateKey:     string(privateKey), | ||||
| 	} | ||||
| 
 | ||||
| 	if secretGitHubAppArgs.baseURL != "" { | ||||
| 		opts.GitHubAppBaseURL = secretGitHubAppArgs.baseURL | ||||
| 	} | ||||
| 
 | ||||
| 	secret, err := sourcesecret.Generate(opts) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	if createArgs.export { | ||||
| 		rootCmd.Println(secret.Content) | ||||
| 		return nil | ||||
| 	} | ||||
| 
 | ||||
| 	ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) | ||||
| 	defer cancel() | ||||
| 	kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	var s corev1.Secret | ||||
| 	if err := yaml.Unmarshal([]byte(secret.Content), &s); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	if err := upsertSecret(ctx, kubeClient, s); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	logger.Actionf("githubapp secret '%s' created in '%s' namespace", secretName, *kubeconfigArgs.Namespace) | ||||
| 	return nil | ||||
| } | ||||
| @ -0,0 +1,74 @@ | ||||
| /* | ||||
| Copyright 2022 The Flux authors | ||||
| 
 | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
| 
 | ||||
|     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| 
 | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| package main | ||||
| 
 | ||||
| import ( | ||||
| 	"testing" | ||||
| ) | ||||
| 
 | ||||
| func TestCreateSecretGitHubApp(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		name   string | ||||
| 		args   string | ||||
| 		assert assertFunc | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name:   "create githubapp secret with missing name", | ||||
| 			args:   "create secret githubapp", | ||||
| 			assert: assertError("name is required"), | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:   "create githubapp secret with missing app-id", | ||||
| 			args:   "create secret githubapp appinfo", | ||||
| 			assert: assertError("--app-id is required"), | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:   "create githubapp secret with missing appInstallationID", | ||||
| 			args:   "create secret githubapp appinfo --app-id 1", | ||||
| 			assert: assertError("--app-installation-id is required"), | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:   "create githubapp secret with missing private key file", | ||||
| 			args:   "create secret githubapp appinfo --app-id 1 --app-installation-id 2", | ||||
| 			assert: assertError("--app-private-key is required"), | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:   "create githubapp secret with private key file that does not exist", | ||||
| 			args:   "create secret githubapp appinfo --app-id 1 --app-installation-id 2 --app-private-key pk.pem", | ||||
| 			assert: assertError("unable to read private key file: open pk.pem: no such file or directory"), | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:   "create githubapp secret with app info", | ||||
| 			args:   "create secret githubapp appinfo --namespace my-namespace --app-id 1 --app-installation-id 2 --app-private-key ./testdata/create_secret/githubapp/test-private-key.pem --export", | ||||
| 			assert: assertGoldenFile("testdata/create_secret/githubapp/secret.yaml"), | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:   "create githubapp secret with appinfo and base url", | ||||
| 			args:   "create secret githubapp appinfo --namespace my-namespace --app-id 1 --app-installation-id 2 --app-private-key ./testdata/create_secret/githubapp/test-private-key.pem --app-base-url www.example.com/api/v3 --export", | ||||
| 			assert: assertGoldenFile("testdata/create_secret/githubapp/secret-with-baseurl.yaml"), | ||||
| 		}, | ||||
| 	} | ||||
| 	for _, tt := range tests { | ||||
| 		t.Run(tt.name, func(t *testing.T) { | ||||
| 			cmd := cmdTestCase{ | ||||
| 				args:   tt.args, | ||||
| 				assert: tt.assert, | ||||
| 			} | ||||
| 			cmd.runTestCmd(t) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
| @ -0,0 +1,39 @@ | ||||
| --- | ||||
| apiVersion: v1 | ||||
| kind: Secret | ||||
| metadata: | ||||
|   name: appinfo | ||||
|   namespace: my-namespace | ||||
| stringData: | ||||
|   githubAppBaseURL: www.example.com/api/v3 | ||||
|   githubAppID: "1" | ||||
|   githubAppInstallationID: "2" | ||||
|   githubAppPrivateKey: |- | ||||
|     -----BEGIN RSA PRIVATE KEY----- | ||||
|     YcE2CgWILk+uiVNseHnOU2frG7k2RJZtdDo8GNI6pQWFlwU/NsQoJBrtEDyYVkap | ||||
|     PLv7VoJ2pr6l5IEwH++naL2McuCcwhW/CeaVb/IuSCFFMwb40zRrlqp6IB5VkMhm | ||||
|     zSde2KD/ilB1YUhaAv0qaBHLTGBvVgxut1eIyvqVC+ArWoqJ7rQTst+Arp3UAiIm | ||||
|     LsXqL2iZvVvCH0FiDwBIfxAMhl6fnPzuQsZBiRLPdD67jubPseN1P5JBRw3WTton | ||||
|     Fa2RLLByuyge7bWh2o1hjEx2w2ZpIhosQRyDs1sPXP5TI92RzOcx1CZaTZ6V5E+T | ||||
|     MROFeZmxBHYon1Y4Rw+jCSXovNyHbMBpMI67nwIDAQABAoIBAC4UrkusU8r7ilFu | ||||
|     w1LWDm09+8WSIk9KgYMoBceAqH+b9DU7hrMFrKkO/cr0Mijr1somv6B3MG83WUcB | ||||
|     FkhEBrwXKnh499iiO/SUo+7kaq0WLQ7mQ2Q9wpMmkkjnr0tgydAno/uNNITSaqmk | ||||
|     YcE2CgWILk+uiVNseHnOU2frG7k2RJZtdDo8GNI6pQWFlwU/NsQoJBrtEDyYVkap | ||||
|     Fa2RLLByuyge7bWh2o1hjEx2w2ZpIhosQRyDs1sPXP5TI92RzOcx1CZaTZ6V5E+T | ||||
|     zSde2KD/ilB1YUhaAv0qaBHLTGBvVgxut1eIyvqVC+ArWoqJ7rQTst+Arp3UAiIm | ||||
|     ihlXNkECgYEA3abZJZuVarHPlAqRYkprs0O+DrP6sPlmVQp+nq8y3Qg00U+N7AuP | ||||
|     Y1riLo3gWq7LajkGTygWLmru2mhWsETxt+R4BtnREUq8kDEoCfEwPlHfqfphvBZL | ||||
|     j5eL60QTKAqSOVqMgIzqJyxa5FGgPGqWpLDLopyVeoyNdZwcuCQzFgkCgYEA25dm | ||||
|     PLv7VoJ2pr6l5IEwH++naL2McuCcwhW/CeaVb/IuSCFFMwb40zRrlqp6IB5VkMhm | ||||
|     MkvaCGIAH+lfJrtTSujFaOIGFy+0ZwP+LNqHUKih14y8Qv9dEP0kaXkAD3fO3Y97 | ||||
|     Nj+Q2c06JpojgBKBMwVvT7M53w9KEoNKpoKBbmcCgYBelHyiRJJsdbVKyXuiAnmU | ||||
|     g/qMkZYOgE1/SjwfgEjm8kJ/cj/wEjq8PaK4FMhAScf46p5blpJoei6zucQL8U9n | ||||
|     lbD102oXw9lUefVI0McyQIN9J58ewDC79AG7gU/fTSt6F75OeFLOJmoedQo33Y+s | ||||
|     dNhf6gsKwQD3x4aluKSn6QKBgD8HbvBAKV6P4vIiFzS0QvWtpeKam2EDHI+h+WsP | ||||
|     nD77QoG/EPvpjJS9/KWgZRPz6U+0M5V0y73MZVzkbbVT/uwfgF2G91lXAr4Kfuh5 | ||||
|     w1LWDm09+8WSIk9KgYMoBceAqH+b9DU7hrMFrKkO/cr0Mijr1somv6B3MG83WUcB | ||||
|     qCEDAoGACl8ClvMJR2uNWdaWnCz9tyPdHYgEusJ0OIP+WUY2ToYQWSlA0zNpc21Y | ||||
|     lbD102oXw9lUefVI0McyQIN9J58ewDC79AG7gU/fTSt6F75OeFLOJmoedQo33Y+s | ||||
|     bUytJtOhHbLRNxwgalhjBUNWICrDktqJmumNOEOOPBqVz7RGwUg= | ||||
|     -----END RSA PRIVATE KEY----- | ||||
| 
 | ||||
| @ -0,0 +1,38 @@ | ||||
| --- | ||||
| apiVersion: v1 | ||||
| kind: Secret | ||||
| metadata: | ||||
|   name: appinfo | ||||
|   namespace: my-namespace | ||||
| stringData: | ||||
|   githubAppID: "1" | ||||
|   githubAppInstallationID: "2" | ||||
|   githubAppPrivateKey: |- | ||||
|     -----BEGIN RSA PRIVATE KEY----- | ||||
|     YcE2CgWILk+uiVNseHnOU2frG7k2RJZtdDo8GNI6pQWFlwU/NsQoJBrtEDyYVkap | ||||
|     PLv7VoJ2pr6l5IEwH++naL2McuCcwhW/CeaVb/IuSCFFMwb40zRrlqp6IB5VkMhm | ||||
|     zSde2KD/ilB1YUhaAv0qaBHLTGBvVgxut1eIyvqVC+ArWoqJ7rQTst+Arp3UAiIm | ||||
|     LsXqL2iZvVvCH0FiDwBIfxAMhl6fnPzuQsZBiRLPdD67jubPseN1P5JBRw3WTton | ||||
|     Fa2RLLByuyge7bWh2o1hjEx2w2ZpIhosQRyDs1sPXP5TI92RzOcx1CZaTZ6V5E+T | ||||
|     MROFeZmxBHYon1Y4Rw+jCSXovNyHbMBpMI67nwIDAQABAoIBAC4UrkusU8r7ilFu | ||||
|     w1LWDm09+8WSIk9KgYMoBceAqH+b9DU7hrMFrKkO/cr0Mijr1somv6B3MG83WUcB | ||||
|     FkhEBrwXKnh499iiO/SUo+7kaq0WLQ7mQ2Q9wpMmkkjnr0tgydAno/uNNITSaqmk | ||||
|     YcE2CgWILk+uiVNseHnOU2frG7k2RJZtdDo8GNI6pQWFlwU/NsQoJBrtEDyYVkap | ||||
|     Fa2RLLByuyge7bWh2o1hjEx2w2ZpIhosQRyDs1sPXP5TI92RzOcx1CZaTZ6V5E+T | ||||
|     zSde2KD/ilB1YUhaAv0qaBHLTGBvVgxut1eIyvqVC+ArWoqJ7rQTst+Arp3UAiIm | ||||
|     ihlXNkECgYEA3abZJZuVarHPlAqRYkprs0O+DrP6sPlmVQp+nq8y3Qg00U+N7AuP | ||||
|     Y1riLo3gWq7LajkGTygWLmru2mhWsETxt+R4BtnREUq8kDEoCfEwPlHfqfphvBZL | ||||
|     j5eL60QTKAqSOVqMgIzqJyxa5FGgPGqWpLDLopyVeoyNdZwcuCQzFgkCgYEA25dm | ||||
|     PLv7VoJ2pr6l5IEwH++naL2McuCcwhW/CeaVb/IuSCFFMwb40zRrlqp6IB5VkMhm | ||||
|     MkvaCGIAH+lfJrtTSujFaOIGFy+0ZwP+LNqHUKih14y8Qv9dEP0kaXkAD3fO3Y97 | ||||
|     Nj+Q2c06JpojgBKBMwVvT7M53w9KEoNKpoKBbmcCgYBelHyiRJJsdbVKyXuiAnmU | ||||
|     g/qMkZYOgE1/SjwfgEjm8kJ/cj/wEjq8PaK4FMhAScf46p5blpJoei6zucQL8U9n | ||||
|     lbD102oXw9lUefVI0McyQIN9J58ewDC79AG7gU/fTSt6F75OeFLOJmoedQo33Y+s | ||||
|     dNhf6gsKwQD3x4aluKSn6QKBgD8HbvBAKV6P4vIiFzS0QvWtpeKam2EDHI+h+WsP | ||||
|     nD77QoG/EPvpjJS9/KWgZRPz6U+0M5V0y73MZVzkbbVT/uwfgF2G91lXAr4Kfuh5 | ||||
|     w1LWDm09+8WSIk9KgYMoBceAqH+b9DU7hrMFrKkO/cr0Mijr1somv6B3MG83WUcB | ||||
|     qCEDAoGACl8ClvMJR2uNWdaWnCz9tyPdHYgEusJ0OIP+WUY2ToYQWSlA0zNpc21Y | ||||
|     lbD102oXw9lUefVI0McyQIN9J58ewDC79AG7gU/fTSt6F75OeFLOJmoedQo33Y+s | ||||
|     bUytJtOhHbLRNxwgalhjBUNWICrDktqJmumNOEOOPBqVz7RGwUg= | ||||
|     -----END RSA PRIVATE KEY----- | ||||
| 
 | ||||
| @ -0,0 +1,27 @@ | ||||
| -----BEGIN RSA PRIVATE KEY----- | ||||
| YcE2CgWILk+uiVNseHnOU2frG7k2RJZtdDo8GNI6pQWFlwU/NsQoJBrtEDyYVkap | ||||
| PLv7VoJ2pr6l5IEwH++naL2McuCcwhW/CeaVb/IuSCFFMwb40zRrlqp6IB5VkMhm | ||||
| zSde2KD/ilB1YUhaAv0qaBHLTGBvVgxut1eIyvqVC+ArWoqJ7rQTst+Arp3UAiIm | ||||
| LsXqL2iZvVvCH0FiDwBIfxAMhl6fnPzuQsZBiRLPdD67jubPseN1P5JBRw3WTton | ||||
| Fa2RLLByuyge7bWh2o1hjEx2w2ZpIhosQRyDs1sPXP5TI92RzOcx1CZaTZ6V5E+T | ||||
| MROFeZmxBHYon1Y4Rw+jCSXovNyHbMBpMI67nwIDAQABAoIBAC4UrkusU8r7ilFu | ||||
| w1LWDm09+8WSIk9KgYMoBceAqH+b9DU7hrMFrKkO/cr0Mijr1somv6B3MG83WUcB | ||||
| FkhEBrwXKnh499iiO/SUo+7kaq0WLQ7mQ2Q9wpMmkkjnr0tgydAno/uNNITSaqmk | ||||
| YcE2CgWILk+uiVNseHnOU2frG7k2RJZtdDo8GNI6pQWFlwU/NsQoJBrtEDyYVkap | ||||
| Fa2RLLByuyge7bWh2o1hjEx2w2ZpIhosQRyDs1sPXP5TI92RzOcx1CZaTZ6V5E+T | ||||
| zSde2KD/ilB1YUhaAv0qaBHLTGBvVgxut1eIyvqVC+ArWoqJ7rQTst+Arp3UAiIm | ||||
| ihlXNkECgYEA3abZJZuVarHPlAqRYkprs0O+DrP6sPlmVQp+nq8y3Qg00U+N7AuP | ||||
| Y1riLo3gWq7LajkGTygWLmru2mhWsETxt+R4BtnREUq8kDEoCfEwPlHfqfphvBZL | ||||
| j5eL60QTKAqSOVqMgIzqJyxa5FGgPGqWpLDLopyVeoyNdZwcuCQzFgkCgYEA25dm | ||||
| PLv7VoJ2pr6l5IEwH++naL2McuCcwhW/CeaVb/IuSCFFMwb40zRrlqp6IB5VkMhm | ||||
| MkvaCGIAH+lfJrtTSujFaOIGFy+0ZwP+LNqHUKih14y8Qv9dEP0kaXkAD3fO3Y97 | ||||
| Nj+Q2c06JpojgBKBMwVvT7M53w9KEoNKpoKBbmcCgYBelHyiRJJsdbVKyXuiAnmU | ||||
| g/qMkZYOgE1/SjwfgEjm8kJ/cj/wEjq8PaK4FMhAScf46p5blpJoei6zucQL8U9n | ||||
| lbD102oXw9lUefVI0McyQIN9J58ewDC79AG7gU/fTSt6F75OeFLOJmoedQo33Y+s | ||||
| dNhf6gsKwQD3x4aluKSn6QKBgD8HbvBAKV6P4vIiFzS0QvWtpeKam2EDHI+h+WsP | ||||
| nD77QoG/EPvpjJS9/KWgZRPz6U+0M5V0y73MZVzkbbVT/uwfgF2G91lXAr4Kfuh5 | ||||
| w1LWDm09+8WSIk9KgYMoBceAqH+b9DU7hrMFrKkO/cr0Mijr1somv6B3MG83WUcB | ||||
| qCEDAoGACl8ClvMJR2uNWdaWnCz9tyPdHYgEusJ0OIP+WUY2ToYQWSlA0zNpc21Y | ||||
| lbD102oXw9lUefVI0McyQIN9J58ewDC79AG7gU/fTSt6F75OeFLOJmoedQo33Y+s | ||||
| bUytJtOhHbLRNxwgalhjBUNWICrDktqJmumNOEOOPBqVz7RGwUg= | ||||
| -----END RSA PRIVATE KEY----- | ||||
| @ -0,0 +1,14 @@ | ||||
| --- | ||||
| apiVersion: source.toolkit.fluxcd.io/v1 | ||||
| kind: GitRepository | ||||
| metadata: | ||||
|   name: podinfo | ||||
|   namespace: flux-system | ||||
| spec: | ||||
|   interval: 1m0s | ||||
|   provider: github | ||||
|   ref: | ||||
|     branch: test | ||||
|   secretRef: | ||||
|     name: appinfo | ||||
|   url: https://github.com/stefanprodan/podinfo | ||||
					Loading…
					
					
				
		Reference in New Issue