diff --git a/cmd/tk/create_source_helm.go b/cmd/tk/create_source_helm.go new file mode 100644 index 00000000..0b1f0e92 --- /dev/null +++ b/cmd/tk/create_source_helm.go @@ -0,0 +1,229 @@ +/* +Copyright 2020 The Flux CD contributors. + +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" + sourcev1 "github.com/fluxcd/source-controller/api/v1alpha1" + "github.com/spf13/cobra" + "io/ioutil" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/wait" + "net/url" + "os" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/yaml" +) + +var createSourceHelmCmd = &cobra.Command{ + Use: "helm [name]", + Short: "Create or update a HelmRepository source", + Long: ` +The create source helm command generates a HelmRepository resource and waits for it to fetch the index. +For private Helm repositories, the basic authentication credentials are stored in a Kubernetes secret.`, + Example: ` # Create a source from a public Helm repository + tk create source helm podinfo \ + --url=https://stefanprodan.github.io/podinfo \ + --interval=10m + + # Create a source from a Helm repository using basic authentication + tk create source helm podinfo \ + --url=https://stefanprodan.github.io/podinfo \ + --username=username \ + --password=password +`, + RunE: createSourceHelmCmdRun, +} + +var ( + sourceHelmURL string + sourceHelmUsername string + sourceHelmPassword string +) + +func init() { + createSourceHelmCmd.Flags().StringVar(&sourceHelmURL, "url", "", "Helm repository address") + createSourceHelmCmd.Flags().StringVarP(&sourceHelmUsername, "username", "u", "", "basic authentication username") + createSourceHelmCmd.Flags().StringVarP(&sourceHelmPassword, "password", "p", "", "basic authentication password") + + createSourceCmd.AddCommand(createSourceHelmCmd) +} + +func createSourceHelmCmdRun(cmd *cobra.Command, args []string) error { + if len(args) < 1 { + return fmt.Errorf("source name is required") + } + name := args[0] + secretName := fmt.Sprintf("helm-%s", name) + + if sourceHelmURL == "" { + return fmt.Errorf("url is required") + } + + tmpDir, err := ioutil.TempDir("", name) + if err != nil { + return err + } + defer os.RemoveAll(tmpDir) + + if _, err := url.Parse(sourceHelmURL); err != nil { + return fmt.Errorf("url parse failed: %w", err) + } + + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + kubeClient, err := utils.kubeClient(kubeconfig) + if err != nil { + return err + } + + helmRepository := sourcev1.HelmRepository{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: sourcev1.HelmRepositorySpec{ + URL: sourceHelmURL, + Interval: metav1.Duration{ + Duration: interval, + }, + }, + } + + if export { + return exportHelmRepository(helmRepository) + } + + withAuth := false + if sourceHelmUsername != "" && sourceHelmPassword != "" { + logger.Actionf("applying secret with basic auth credentials") + secret := corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: namespace, + }, + StringData: map[string]string{ + "username": sourceHelmUsername, + "password": sourceHelmPassword, + }, + } + if err := upsertSecret(ctx, kubeClient, secret); err != nil { + return err + } + withAuth = true + } + + if withAuth { + logger.Successf("authentication configured") + } + + logger.Generatef("generating source") + + if withAuth { + helmRepository.Spec.SecretRef = &corev1.LocalObjectReference{ + Name: secretName, + } + } + + logger.Actionf("applying source") + if err := upsertHelmRepository(ctx, kubeClient, helmRepository); err != nil { + return err + } + + logger.Waitingf("waiting for index download") + if err := wait.PollImmediate(pollInterval, timeout, + isHelmRepositoryReady(ctx, kubeClient, name, namespace)); err != nil { + return err + } + + logger.Successf("index download completed") + + namespacedName := types.NamespacedName{ + Namespace: namespace, + Name: name, + } + err = kubeClient.Get(ctx, namespacedName, &helmRepository) + if err != nil { + return fmt.Errorf("helm index failed: %w", err) + } + + if helmRepository.Status.Artifact != nil { + logger.Successf("fetched revision: %s", helmRepository.Status.Artifact.Revision) + } else { + return fmt.Errorf("index download failed, artifact not found") + } + + return nil +} + +func upsertHelmRepository(ctx context.Context, kubeClient client.Client, helmRepository sourcev1.HelmRepository) error { + namespacedName := types.NamespacedName{ + Namespace: helmRepository.GetNamespace(), + Name: helmRepository.GetName(), + } + + var existing sourcev1.HelmRepository + err := kubeClient.Get(ctx, namespacedName, &existing) + if err != nil { + if errors.IsNotFound(err) { + if err := kubeClient.Create(ctx, &helmRepository); err != nil { + return err + } else { + logger.Successf("source created") + return nil + } + } + return err + } + + existing.Spec = helmRepository.Spec + if err := kubeClient.Update(ctx, &existing); err != nil { + return err + } + + logger.Successf("source updated") + return nil +} + +func exportHelmRepository(source sourcev1.HelmRepository) error { + gvk := sourcev1.GroupVersion.WithKind(sourcev1.HelmRepositoryKind) + export := sourcev1.HelmRepository{ + TypeMeta: metav1.TypeMeta{ + Kind: gvk.Kind, + APIVersion: gvk.GroupVersion().String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: source.Name, + Namespace: source.Namespace, + }, + Spec: source.Spec, + } + + data, err := yaml.Marshal(export) + if err != nil { + return err + } + + fmt.Println("---") + fmt.Println(string(data)) + return nil +} diff --git a/cmd/tk/delete_source_helm.go b/cmd/tk/delete_source_helm.go new file mode 100644 index 00000000..6dbe8397 --- /dev/null +++ b/cmd/tk/delete_source_helm.go @@ -0,0 +1,86 @@ +/* +Copyright 2020 The Flux CD contributors. + +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" + + sourcev1 "github.com/fluxcd/source-controller/api/v1alpha1" + "github.com/manifoldco/promptui" + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/types" +) + +var deleteSourceHelmCmd = &cobra.Command{ + Use: "helm [name]", + Short: "Delete a HelmRepository source", + Long: "The delete source helm command deletes the given HelmRepository from the cluster.", + Example: ` # Delete a Helm repository + tk delete source helm podinfo +`, + RunE: deleteSourceHelmCmdRun, +} + +func init() { + deleteSourceCmd.AddCommand(deleteSourceHelmCmd) +} + +func deleteSourceHelmCmdRun(cmd *cobra.Command, args []string) error { + if len(args) < 1 { + return fmt.Errorf("name is required") + } + name := args[0] + + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + kubeClient, err := utils.kubeClient(kubeconfig) + if err != nil { + return err + } + + namespacedName := types.NamespacedName{ + Namespace: namespace, + Name: name, + } + + var helmRepository sourcev1.HelmRepository + err = kubeClient.Get(ctx, namespacedName, &helmRepository) + if err != nil { + return err + } + + if !deleteSilent { + prompt := promptui.Prompt{ + Label: "Are you sure you want to delete this source", + IsConfirm: true, + } + if _, err := prompt.Run(); err != nil { + return fmt.Errorf("aborting") + } + } + + logger.Actionf("deleting source %s in %s namespace", name, namespace) + err = kubeClient.Delete(ctx, &helmRepository) + if err != nil { + return err + } + logger.Successf("source deleted") + + return nil +} diff --git a/cmd/tk/export_source_helm.go b/cmd/tk/export_source_helm.go new file mode 100644 index 00000000..1026693c --- /dev/null +++ b/cmd/tk/export_source_helm.go @@ -0,0 +1,139 @@ +/* +Copyright 2020 The Flux CD contributors. + +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" + + sourcev1 "github.com/fluxcd/source-controller/api/v1alpha1" + "github.com/spf13/cobra" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/yaml" +) + +var exportSourceHelmCmd = &cobra.Command{ + Use: "helm [name]", + Short: "Export HelmRepository sources in YAML format", + Long: "The export source git command exports on or all HelmRepository sources in YAML format.", + Example: ` # Export all HelmRepository sources + tk export source helm --all > sources.yaml + + # Export a HelmRepository source including the basic auth credentials + tk export source helm my-private-repo --with-credentials > source.yaml +`, + RunE: exportSourceHelmCmdRun, +} + +func init() { + exportSourceCmd.AddCommand(exportSourceHelmCmd) +} + +func exportSourceHelmCmdRun(cmd *cobra.Command, args []string) error { + if !exportAll && len(args) < 1 { + return fmt.Errorf("name is required") + } + + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + kubeClient, err := utils.kubeClient(kubeconfig) + if err != nil { + return err + } + + if exportAll { + var list sourcev1.HelmRepositoryList + err = kubeClient.List(ctx, &list, client.InNamespace(namespace)) + if err != nil { + return err + } + + if len(list.Items) == 0 { + logger.Failuref("no source found in %s namespace", namespace) + return nil + } + + for _, repository := range list.Items { + if err := exportHelmRepository(repository); err != nil { + return err + } + if exportSourceWithCred { + if err := exportHelmCredentials(ctx, kubeClient, repository); err != nil { + return err + } + } + } + } else { + name := args[0] + namespacedName := types.NamespacedName{ + Namespace: namespace, + Name: name, + } + var repository sourcev1.HelmRepository + err = kubeClient.Get(ctx, namespacedName, &repository) + if err != nil { + return err + } + if err := exportHelmRepository(repository); err != nil { + return err + } + if exportSourceWithCred { + return exportHelmCredentials(ctx, kubeClient, repository) + } + } + return nil +} + +func exportHelmCredentials(ctx context.Context, kubeClient client.Client, source sourcev1.HelmRepository) error { + if source.Spec.SecretRef != nil { + namespacedName := types.NamespacedName{ + Namespace: source.Namespace, + Name: source.Spec.SecretRef.Name, + } + var cred corev1.Secret + err := kubeClient.Get(ctx, namespacedName, &cred) + if err != nil { + return fmt.Errorf("failed to retrieve secret %s, error: %w", namespacedName.Name, err) + } + + exported := corev1.Secret{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Secret", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: namespacedName.Name, + Namespace: namespacedName.Namespace, + }, + Data: cred.Data, + Type: cred.Type, + } + + data, err := yaml.Marshal(exported) + if err != nil { + return err + } + + fmt.Println("---") + fmt.Println(string(data)) + } + return nil +} diff --git a/cmd/tk/get_source_helm.go b/cmd/tk/get_source_helm.go new file mode 100644 index 00000000..04d46635 --- /dev/null +++ b/cmd/tk/get_source_helm.go @@ -0,0 +1,80 @@ +/* +Copyright 2020 The Flux CD contributors. + +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" + + sourcev1 "github.com/fluxcd/source-controller/api/v1alpha1" + "github.com/spf13/cobra" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +var getSourceHelmCmd = &cobra.Command{ + Use: "helm", + Short: "Get HelmRepository source statuses", + Long: "The get sources helm command prints the status of the HelmRepository sources.", + Example: ` # List all Helm repositories and their status + tk get sources helm +`, + RunE: getSourceHelmCmdRun, +} + +func init() { + getSourceCmd.AddCommand(getSourceHelmCmd) +} + +func getSourceHelmCmdRun(cmd *cobra.Command, args []string) error { + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + kubeClient, err := utils.kubeClient(kubeconfig) + if err != nil { + return err + } + + var list sourcev1.HelmRepositoryList + err = kubeClient.List(ctx, &list, client.InNamespace(namespace)) + if err != nil { + return err + } + + if len(list.Items) == 0 { + logger.Failuref("no sources found in %s namespace", namespace) + return nil + } + + for _, source := range list.Items { + isInitialized := false + for _, condition := range source.Status.Conditions { + if condition.Type == sourcev1.ReadyCondition { + if condition.Status != corev1.ConditionFalse { + logger.Successf("%s last fetched revision: %s", source.GetName(), source.Status.Artifact.Revision) + } else { + logger.Failuref("%s %s", source.GetName(), condition.Message) + } + isInitialized = true + break + } + } + if !isInitialized { + logger.Failuref("%s is not ready", source.GetName()) + } + } + return nil +}