From d1ac6c1c13c9cd87926d7bcd257c1649f873e921 Mon Sep 17 00:00:00 2001 From: Greg Fichtenholtz <74032303+gfichtenholt@users.noreply.github.com> Date: Wed, 13 Apr 2022 21:16:42 -0700 Subject: [PATCH] 5th step for Investigate and propose package repositories API with similar core interface to packages API. #3496 (#4587) * attempt #2 * step 1 * 2nd step --- .../packages/v1alpha1/global_vars_test.go | 508 ++++++++++++++++++ .../fluxv2/packages/v1alpha1/release_test.go | 2 + .../plugins/fluxv2/packages/v1alpha1/repo.go | 459 ++++++++++------ .../v1alpha1/repo_integration_test.go | 214 +++++++- .../fluxv2/packages/v1alpha1/repo_test.go | 311 ++++++++++- .../fluxv2/packages/v1alpha1/server.go | 23 +- 6 files changed, 1334 insertions(+), 183 deletions(-) diff --git a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/global_vars_test.go b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/global_vars_test.go index 01270cb1aa5..bc94ad2eaab 100644 --- a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/global_vars_test.go +++ b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/global_vars_test.go @@ -1110,6 +1110,23 @@ var ( }, } + add_repo_5 = sourcev1.HelmRepository{ + TypeMeta: metav1.TypeMeta{ + Kind: sourcev1.HelmRepositoryKind, + APIVersion: sourcev1.GroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "bar", + Namespace: "foo", + ResourceVersion: "1", + }, + Spec: sourcev1.HelmRepositorySpec{ + URL: "http://example.com", + Interval: metav1.Duration{Duration: 10 * time.Minute}, + PassCredentials: true, + }, + } + add_repo_req_1 = &corev1.AddPackageRepositoryRequest{ Name: "bar", Context: &corev1.Context{Namespace: "foo"}, @@ -1358,6 +1375,16 @@ var ( }, } + add_repo_req_20 = &corev1.AddPackageRepositoryRequest{ + Name: "bar", + Context: &corev1.Context{Namespace: "foo"}, + Type: "helm", + Url: "http://example.com", + Auth: &corev1.PackageRepositoryAuth{ + PassCredentials: true, + }, + } + add_repo_expected_resp = &corev1.AddPackageRepositoryResponse{ PackageRepoRef: &corev1.PackageRepositoryReference{ Context: &corev1.Context{ @@ -3209,4 +3236,485 @@ var ( }, } } + + update_repo_req_1 = &corev1.UpdatePackageRepositoryRequest{ + PackageRepoRef: &corev1.PackageRepositoryReference{ + Context: &corev1.Context{ + Namespace: "namespace-1", + }, + Identifier: "repo-1", + }, + Url: "http://newurl.com", + } + + update_repo_req_2 = &corev1.UpdatePackageRepositoryRequest{ + PackageRepoRef: &corev1.PackageRepositoryReference{ + Context: &corev1.Context{ + Namespace: "namespace-1", + }, + Identifier: "repo-1", + }, + Url: "https://example.repo.com/charts", + Interval: 345, + } + + update_repo_req_3 = &corev1.UpdatePackageRepositoryRequest{ + PackageRepoRef: &corev1.PackageRepositoryReference{ + Context: &corev1.Context{ + Namespace: "namespace-1", + }, + Identifier: "repo-1", + }, + Url: "https://example.repo.com/charts", + Auth: &corev1.PackageRepositoryAuth{ + PassCredentials: true, + }, + } + + update_repo_req_4 = &corev1.UpdatePackageRepositoryRequest{ + PackageRepoRef: &corev1.PackageRepositoryReference{ + Context: &corev1.Context{ + Namespace: "namespace-1", + }, + Identifier: "repo-1", + }, + Url: "https://example.repo.com/charts", + TlsConfig: &corev1.PackageRepositoryTlsConfig{ + PackageRepoTlsConfigOneOf: &corev1.PackageRepositoryTlsConfig_SecretRef{ + SecretRef: &corev1.SecretKeyReference{ + Name: "secret-1", + Key: "caFile", + }, + }, + }, + } + + update_repo_req_5 = &corev1.UpdatePackageRepositoryRequest{ + PackageRepoRef: &corev1.PackageRepositoryReference{ + Context: &corev1.Context{ + Namespace: "namespace-1", + }, + Identifier: "repo-1", + }, + Url: "https://example.repo.com/charts", + } + + update_repo_req_6 = &corev1.UpdatePackageRepositoryRequest{ + PackageRepoRef: &corev1.PackageRepositoryReference{ + Context: &corev1.Context{ + Namespace: "namespace-1", + }, + Identifier: "repo-1", + }, + Url: "https://example.repo.com/charts", + Auth: &corev1.PackageRepositoryAuth{ + PackageRepoAuthOneOf: &corev1.PackageRepositoryAuth_SecretRef{ + SecretRef: &corev1.SecretKeyReference{ + Name: "secret-1", + }, + }, + }, + } + + update_repo_req_7 = &corev1.UpdatePackageRepositoryRequest{ + PackageRepoRef: &corev1.PackageRepositoryReference{ + Context: &corev1.Context{ + Namespace: "namespace-1", + }, + Identifier: "repo-1", + }, + Url: "https://example.repo.com/charts", + } + + update_repo_req_8 = func(pub, priv []byte) *corev1.UpdatePackageRepositoryRequest { + return &corev1.UpdatePackageRepositoryRequest{ + PackageRepoRef: &corev1.PackageRepositoryReference{ + Context: &corev1.Context{ + Namespace: "namespace-1", + }, + Identifier: "repo-1", + }, + Url: "https://example.repo.com/charts", + Auth: &corev1.PackageRepositoryAuth{ + Type: corev1.PackageRepositoryAuth_PACKAGE_REPOSITORY_AUTH_TYPE_TLS, + PackageRepoAuthOneOf: &corev1.PackageRepositoryAuth_TlsCertKey{ + TlsCertKey: &corev1.TlsCertKey{ + Cert: string(pub), + Key: string(priv), + }, + }, + }, + } + } + + update_repo_req_9 = &corev1.UpdatePackageRepositoryRequest{ + PackageRepoRef: &corev1.PackageRepositoryReference{ + Context: &corev1.Context{ + Namespace: "namespace-1", + }, + Identifier: "repo-1", + }, + Url: "https://example.repo.com/charts", + } + + update_repo_req_10 = &corev1.UpdatePackageRepositoryRequest{ + PackageRepoRef: &corev1.PackageRepositoryReference{ + Context: &corev1.Context{ + Namespace: "namespace-1", + }, + Identifier: "repo-1", + }, + Url: "https://example.repo.com/charts", + Auth: &corev1.PackageRepositoryAuth{ + Type: corev1.PackageRepositoryAuth_PACKAGE_REPOSITORY_AUTH_TYPE_BASIC_AUTH, + PackageRepoAuthOneOf: &corev1.PackageRepositoryAuth_UsernamePassword{ + UsernamePassword: &corev1.UsernamePassword{ + Username: "foo", + Password: "bar", + }, + }, + }, + } + + update_repo_req_11 = &corev1.UpdatePackageRepositoryRequest{ + PackageRepoRef: &corev1.PackageRepositoryReference{ + Context: &corev1.Context{ + Namespace: "TBD", + }, + Identifier: "my-podinfo", + }, + Url: podinfo_basic_auth_repo_url, + Auth: &corev1.PackageRepositoryAuth{ + Type: corev1.PackageRepositoryAuth_PACKAGE_REPOSITORY_AUTH_TYPE_BASIC_AUTH, + PackageRepoAuthOneOf: &corev1.PackageRepositoryAuth_UsernamePassword{ + UsernamePassword: &corev1.UsernamePassword{ + Username: "foo", + Password: "bar", + }, + }, + }, + } + + update_repo_req_12 = &corev1.UpdatePackageRepositoryRequest{ + PackageRepoRef: &corev1.PackageRepositoryReference{ + Context: &corev1.Context{ + Namespace: "TBD", + }, + Identifier: "my-podinfo-2", + }, + Url: podinfo_basic_auth_repo_url, + Auth: &corev1.PackageRepositoryAuth{ + Type: corev1.PackageRepositoryAuth_PACKAGE_REPOSITORY_AUTH_TYPE_BASIC_AUTH, + PackageRepoAuthOneOf: &corev1.PackageRepositoryAuth_UsernamePassword{ + UsernamePassword: &corev1.UsernamePassword{ + Username: "foo", + Password: "bar", + }, + }, + }, + } + + update_repo_resp_1 = &corev1.UpdatePackageRepositoryResponse{ + PackageRepoRef: &corev1.PackageRepositoryReference{ + Context: &corev1.Context{ + Namespace: "namespace-1", + Cluster: KubeappsCluster, + }, + Identifier: "repo-1", + Plugin: fluxPlugin, + }, + } + + update_repo_resp_2 = &corev1.UpdatePackageRepositoryResponse{ + PackageRepoRef: &corev1.PackageRepositoryReference{ + Context: &corev1.Context{ + Namespace: "TBD", + Cluster: KubeappsCluster, + }, + Identifier: "my-podinfo", + Plugin: fluxPlugin, + }, + } + + update_repo_resp_3 = &corev1.UpdatePackageRepositoryResponse{ + PackageRepoRef: &corev1.PackageRepositoryReference{ + Context: &corev1.Context{ + Namespace: "TBD", + Cluster: KubeappsCluster, + }, + Identifier: "my-podinfo-2", + Plugin: fluxPlugin, + }, + } + + update_repo_detail_1 = &corev1.GetPackageRepositoryDetailResponse{ + Detail: &corev1.PackageRepositoryDetail{ + PackageRepoRef: get_repo_detail_package_resp_ref, + Name: "repo-1", + Description: "", + NamespaceScoped: false, + Type: "helm", + Url: "http://newurl.com", + Interval: 600, + Auth: &corev1.PackageRepositoryAuth{ + PassCredentials: false, + }, + Status: &corev1.PackageRepositoryStatus{ + Reason: corev1.PackageRepositoryStatus_STATUS_REASON_PENDING, + }, + }, + } + + update_repo_detail_2 = &corev1.GetPackageRepositoryDetailResponse{ + Detail: &corev1.PackageRepositoryDetail{ + PackageRepoRef: get_repo_detail_package_resp_ref, + Name: "repo-1", + Description: "", + NamespaceScoped: false, + Type: "helm", + Url: "https://example.repo.com/charts", + Interval: 345, + Auth: &corev1.PackageRepositoryAuth{ + PassCredentials: false, + }, + Status: &corev1.PackageRepositoryStatus{ + Reason: corev1.PackageRepositoryStatus_STATUS_REASON_PENDING, + }, + }, + } + + update_repo_detail_3 = &corev1.GetPackageRepositoryDetailResponse{ + Detail: &corev1.PackageRepositoryDetail{ + PackageRepoRef: get_repo_detail_package_resp_ref, + Name: "repo-1", + Description: "", + NamespaceScoped: false, + Type: "helm", + Url: "https://example.repo.com/charts", + Interval: 600, + Auth: &corev1.PackageRepositoryAuth{ + PassCredentials: true, + }, + Status: &corev1.PackageRepositoryStatus{ + Reason: corev1.PackageRepositoryStatus_STATUS_REASON_PENDING, + }, + }, + } + + update_repo_detail_4 = &corev1.GetPackageRepositoryDetailResponse{ + Detail: &corev1.PackageRepositoryDetail{ + PackageRepoRef: get_repo_detail_package_resp_ref, + Name: "repo-1", + Description: "", + NamespaceScoped: false, + Type: "helm", + Url: "https://example.repo.com/charts", + Interval: 600, + Auth: &corev1.PackageRepositoryAuth{ + PassCredentials: false, + }, + TlsConfig: &corev1.PackageRepositoryTlsConfig{ + PackageRepoTlsConfigOneOf: &corev1.PackageRepositoryTlsConfig_SecretRef{ + SecretRef: &corev1.SecretKeyReference{ + Name: "secret-1", + Key: "caFile", + }, + }, + }, + Status: &corev1.PackageRepositoryStatus{ + Reason: corev1.PackageRepositoryStatus_STATUS_REASON_PENDING, + }, + }, + } + + update_repo_detail_5 = &corev1.GetPackageRepositoryDetailResponse{ + Detail: &corev1.PackageRepositoryDetail{ + PackageRepoRef: get_repo_detail_package_resp_ref, + Name: "repo-1", + Description: "", + NamespaceScoped: false, + Type: "helm", + Url: "https://example.repo.com/charts", + Interval: 600, + Auth: &corev1.PackageRepositoryAuth{ + PassCredentials: false, + }, + Status: &corev1.PackageRepositoryStatus{ + Reason: corev1.PackageRepositoryStatus_STATUS_REASON_PENDING, + }, + }, + } + + update_repo_detail_6 = &corev1.GetPackageRepositoryDetailResponse{ + Detail: &corev1.PackageRepositoryDetail{ + PackageRepoRef: get_repo_detail_package_resp_ref, + Name: "repo-1", + Description: "", + NamespaceScoped: false, + Type: "helm", + Url: "https://example.repo.com/charts", + Interval: 600, + Auth: &corev1.PackageRepositoryAuth{ + Type: corev1.PackageRepositoryAuth_PACKAGE_REPOSITORY_AUTH_TYPE_BASIC_AUTH, + PackageRepoAuthOneOf: &corev1.PackageRepositoryAuth_SecretRef{ + SecretRef: &corev1.SecretKeyReference{ + Name: "secret-1", + }, + }, + }, + Status: &corev1.PackageRepositoryStatus{ + Reason: corev1.PackageRepositoryStatus_STATUS_REASON_PENDING, + }, + }, + } + + update_repo_detail_7 = &corev1.GetPackageRepositoryDetailResponse{ + Detail: &corev1.PackageRepositoryDetail{ + PackageRepoRef: get_repo_detail_package_resp_ref, + Name: "repo-1", + Description: "", + NamespaceScoped: false, + Type: "helm", + Url: "https://example.repo.com/charts", + Interval: 600, + Auth: &corev1.PackageRepositoryAuth{}, + Status: &corev1.PackageRepositoryStatus{ + Reason: corev1.PackageRepositoryStatus_STATUS_REASON_PENDING, + }, + }, + } + + update_repo_detail_8 = func(pub, priv []byte) *corev1.GetPackageRepositoryDetailResponse { + return &corev1.GetPackageRepositoryDetailResponse{ + Detail: &corev1.PackageRepositoryDetail{ + PackageRepoRef: get_repo_detail_package_resp_ref, + Name: "repo-1", + Description: "", + NamespaceScoped: false, + Type: "helm", + Url: "https://example.repo.com/charts", + Interval: 600, + Auth: &corev1.PackageRepositoryAuth{ + Type: corev1.PackageRepositoryAuth_PACKAGE_REPOSITORY_AUTH_TYPE_TLS, + PackageRepoAuthOneOf: &corev1.PackageRepositoryAuth_TlsCertKey{ + TlsCertKey: &corev1.TlsCertKey{ + Cert: string(pub), + Key: string(priv), + }, + }, + }, + Status: &corev1.PackageRepositoryStatus{ + Reason: corev1.PackageRepositoryStatus_STATUS_REASON_PENDING, + }, + }, + } + } + + update_repo_detail_9 = &corev1.GetPackageRepositoryDetailResponse{ + Detail: &corev1.PackageRepositoryDetail{ + PackageRepoRef: get_repo_detail_package_resp_ref, + Name: "repo-1", + Description: "", + NamespaceScoped: false, + Type: "helm", + Url: "https://example.repo.com/charts", + Interval: 600, + Auth: &corev1.PackageRepositoryAuth{}, + Status: &corev1.PackageRepositoryStatus{ + Reason: corev1.PackageRepositoryStatus_STATUS_REASON_PENDING, + }, + }, + } + + update_repo_detail_10 = &corev1.GetPackageRepositoryDetailResponse{ + Detail: &corev1.PackageRepositoryDetail{ + PackageRepoRef: get_repo_detail_package_resp_ref, + Name: "repo-1", + Description: "", + NamespaceScoped: false, + Type: "helm", + Url: "https://example.repo.com/charts", + Interval: 600, + Auth: &corev1.PackageRepositoryAuth{ + Type: corev1.PackageRepositoryAuth_PACKAGE_REPOSITORY_AUTH_TYPE_BASIC_AUTH, + PackageRepoAuthOneOf: &corev1.PackageRepositoryAuth_UsernamePassword{ + UsernamePassword: &corev1.UsernamePassword{ + Username: "foo", + Password: "bar", + }, + }, + }, + Status: &corev1.PackageRepositoryStatus{ + Reason: corev1.PackageRepositoryStatus_STATUS_REASON_PENDING, + }, + }, + } + + update_repo_detail_11 = &corev1.GetPackageRepositoryDetailResponse{ + Detail: &corev1.PackageRepositoryDetail{ + PackageRepoRef: &corev1.PackageRepositoryReference{ + Context: &corev1.Context{ + Cluster: KubeappsCluster, + // will be set when scenario is run + Namespace: "TBD", + }, + Identifier: "my-podinfo", + Plugin: fluxPlugin, + }, + Name: "my-podinfo", + Description: "", + NamespaceScoped: false, + Type: "helm", + Url: podinfo_basic_auth_repo_url, + Interval: 600, + Auth: &corev1.PackageRepositoryAuth{ + Type: corev1.PackageRepositoryAuth_PACKAGE_REPOSITORY_AUTH_TYPE_BASIC_AUTH, + PackageRepoAuthOneOf: &corev1.PackageRepositoryAuth_UsernamePassword{ + UsernamePassword: &corev1.UsernamePassword{ + Username: "foo", + Password: "bar", + }, + }, + }, + Status: &corev1.PackageRepositoryStatus{ + Ready: true, + Reason: corev1.PackageRepositoryStatus_STATUS_REASON_SUCCESS, + UserReason: "Succeeded: stored artifact for revision '9d3ac1eb708dfaebae14d7c88fd46afce8b1e0f7aace790d91758575dc8ce518'", + }, + }, + } + + update_repo_detail_12 = &corev1.GetPackageRepositoryDetailResponse{ + Detail: &corev1.PackageRepositoryDetail{ + PackageRepoRef: &corev1.PackageRepositoryReference{ + Context: &corev1.Context{ + Cluster: KubeappsCluster, + // will be set when scenario is run + Namespace: "TBD", + }, + Identifier: "my-podinfo-2", + Plugin: fluxPlugin, + }, + Name: "my-podinfo-2", + Description: "", + NamespaceScoped: false, + Type: "helm", + Url: podinfo_basic_auth_repo_url, + Interval: 600, + Auth: &corev1.PackageRepositoryAuth{ + Type: corev1.PackageRepositoryAuth_PACKAGE_REPOSITORY_AUTH_TYPE_BASIC_AUTH, + PackageRepoAuthOneOf: &corev1.PackageRepositoryAuth_UsernamePassword{ + UsernamePassword: &corev1.UsernamePassword{ + Username: "foo", + Password: "bar", + }, + }, + }, + Status: &corev1.PackageRepositoryStatus{ + Ready: true, + Reason: corev1.PackageRepositoryStatus_STATUS_REASON_SUCCESS, + UserReason: "Succeeded: stored artifact for revision '9d3ac1eb708dfaebae14d7c88fd46afce8b1e0f7aace790d91758575dc8ce518'", + }, + }, + } ) diff --git a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/release_test.go b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/release_test.go index 0111dc84c1a..5a580c6a3e9 100644 --- a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/release_test.go +++ b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/release_test.go @@ -825,6 +825,8 @@ func TestUpdateInstalledPackage(t *testing.T) { expectedRelease: flux_helm_release_updated_upgrade_patch, defaultUpgradePolicyStr: "patch", }, + // TODO test case: update installed package that is pending reconciliation + // TODO test case: update installed package that has failed reconciliation } for _, tc := range testCases { diff --git a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/repo.go b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/repo.go index 60d88e9f7f0..273127d9261 100644 --- a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/repo.go +++ b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/repo.go @@ -213,14 +213,14 @@ func (s *Server) newRepo(ctx context.Context, targetName types.NamespacedName, u return nil, err } } else { - if secretRef, err = s.setupRepoKubeappsManagedSecrets(ctx, targetName, tlsConfig, auth); err != nil { + if secretRef, err = s.createRepoKubeappsManagedSecrets(ctx, targetName, tlsConfig, auth); err != nil { return nil, err } } passCredentials := auth != nil && auth.PassCredentials - if fluxRepo, err := s.newFluxHelmRepo(targetName, url, interval, secretRef, passCredentials); err != nil { + if fluxRepo, err := newFluxHelmRepo(targetName, url, interval, secretRef, passCredentials); err != nil { return nil, err } else if client, err := s.getClient(ctx, targetName.Namespace); err != nil { return nil, err @@ -238,42 +238,6 @@ func (s *Server) newRepo(ctx context.Context, targetName types.NamespacedName, u } } -// ref https://fluxcd.io/docs/components/source/helmrepositories/ -func (s *Server) newFluxHelmRepo( - targetName types.NamespacedName, - url string, - interval uint32, - secretRef string, - passCredentials bool) (*sourcev1.HelmRepository, error) { - pollInterval := defaultPollInterval - if interval > 0 { - pollInterval = metav1.Duration{Duration: time.Duration(interval) * time.Second} - } - fluxRepo := &sourcev1.HelmRepository{ - TypeMeta: metav1.TypeMeta{ - Kind: sourcev1.HelmRepositoryKind, - APIVersion: sourcev1.GroupVersion.String(), - }, - ObjectMeta: metav1.ObjectMeta{ - Name: targetName.Name, - Namespace: targetName.Namespace, - }, - Spec: sourcev1.HelmRepositorySpec{ - URL: url, - Interval: pollInterval, - }, - } - if secretRef != "" { - fluxRepo.Spec.SecretRef = &fluxmeta.LocalObjectReference{ - Name: secretRef, - } - } - if passCredentials { - fluxRepo.Spec.PassCredentials = true - } - return fluxRepo, nil -} - func (s *Server) repoDetail(ctx context.Context, repoRef *corev1.PackageRepositoryReference) (*corev1.PackageRepositoryDetail, error) { key := types.NamespacedName{Namespace: repoRef.Context.Namespace, Name: repoRef.Identifier} @@ -299,11 +263,11 @@ func (s *Server) repoDetail(ctx context.Context, repoRef *corev1.PackageReposito } if s.pluginConfig.UserManagedSecrets { - if tlsConfig, auth, err = s.getRepoTlsConfigAndAuthWithUserManagedSecrets(secret); err != nil { + if tlsConfig, auth, err = getRepoTlsConfigAndAuthWithUserManagedSecrets(secret); err != nil { return nil, err } } else { - if tlsConfig, auth, err = s.getRepoTlsConfigAndAuthWithKubeappsManagedSecrets(secret); err != nil { + if tlsConfig, auth, err = getRepoTlsConfigAndAuthWithKubeappsManagedSecrets(secret); err != nil { return nil, err } } @@ -440,56 +404,15 @@ func (s *Server) validateRepoUserManagedSecrets( return secretRef, nil } -func (s *Server) setupRepoKubeappsManagedSecrets( +func (s *Server) createRepoKubeappsManagedSecrets( ctx context.Context, repoName types.NamespacedName, tlsConfig *corev1.PackageRepositoryTlsConfig, auth *corev1.PackageRepositoryAuth) (string, error) { - var secret *apiv1.Secret - if tlsConfig != nil { - if tlsConfig.GetSecretRef() != nil { - return "", status.Errorf(codes.InvalidArgument, "SecretRef may not be used with kubeapps managed secrets") - } - caCert := tlsConfig.GetCertAuthority() - if caCert != "" { - secret = common.NewLocalOpaqueSecret(repoName.Name + "-") - secret.Data["caFile"] = []byte(caCert) - } - } - if auth != nil { - if auth.GetSecretRef() != nil { - return "", status.Errorf(codes.InvalidArgument, "SecretRef may not be used with kubeapps managed secrets") - } - if secret == nil { - secret = common.NewLocalOpaqueSecret(repoName.Name + "-") - } - switch auth.Type { - case corev1.PackageRepositoryAuth_PACKAGE_REPOSITORY_AUTH_TYPE_BASIC_AUTH: - if unp := auth.GetUsernamePassword(); unp != nil { - secret.Data["username"] = []byte(unp.Username) - secret.Data["password"] = []byte(unp.Password) - } else { - return "", status.Errorf(codes.Internal, "Username/Password configuration is missing") - } - case corev1.PackageRepositoryAuth_PACKAGE_REPOSITORY_AUTH_TYPE_TLS: - if ck := auth.GetTlsCertKey(); ck != nil { - secret.Data["certFile"] = []byte(ck.Cert) - secret.Data["keyFile"] = []byte(ck.Key) - } else { - return "", status.Errorf(codes.Internal, "TLS Cert/Key configuration is missing") - } - case corev1.PackageRepositoryAuth_PACKAGE_REPOSITORY_AUTH_TYPE_BEARER, corev1.PackageRepositoryAuth_PACKAGE_REPOSITORY_AUTH_TYPE_CUSTOM: - return "", status.Errorf(codes.Unimplemented, "Package repository authentication type %q is not supported", auth.Type) - case corev1.PackageRepositoryAuth_PACKAGE_REPOSITORY_AUTH_TYPE_DOCKER_CONFIG_JSON: - if dc := auth.GetDockerCreds(); dc != nil { - secret.Type = apiv1.SecretTypeDockerConfigJson - secret.Data[".dockerconfigjson"] = common.DockerCredentialsToSecretData(dc) - } else { - return "", status.Errorf(codes.Internal, "Docker credentials configuration is missing") - } - default: - return "", status.Errorf(codes.Internal, "Unexpected package repository authentication type: %q", auth.Type) - } + + secret, err := newSecretFromTlsConfigAndAuth(repoName, tlsConfig, auth) + if err != nil { + return "", err } secretRef := "" @@ -506,98 +429,129 @@ func (s *Server) setupRepoKubeappsManagedSecrets( return secretRef, nil } -func (s *Server) getRepoTlsConfigAndAuthWithUserManagedSecrets(secret *apiv1.Secret) (*corev1.PackageRepositoryTlsConfig, *corev1.PackageRepositoryAuth, error) { - var tlsConfig *corev1.PackageRepositoryTlsConfig - auth := &corev1.PackageRepositoryAuth{ - Type: corev1.PackageRepositoryAuth_PACKAGE_REPOSITORY_AUTH_TYPE_UNSPECIFIED, +func (s *Server) updateRepoKubeappsManagedSecrets( + ctx context.Context, + repoName types.NamespacedName, + tlsConfig *corev1.PackageRepositoryTlsConfig, + auth *corev1.PackageRepositoryAuth, + existingSecretRef *fluxmeta.LocalObjectReference) (string, error) { + + secret, err := newSecretFromTlsConfigAndAuth(repoName, tlsConfig, auth) + if err != nil { + return "", err } - if _, ok := secret.Data["caFile"]; ok { - tlsConfig = &corev1.PackageRepositoryTlsConfig{ - // flux plug in doesn't support this option - InsecureSkipVerify: false, - PackageRepoTlsConfigOneOf: &corev1.PackageRepositoryTlsConfig_SecretRef{ - SecretRef: &corev1.SecretKeyReference{ - Name: secret.Name, - Key: "caFile", - }, - }, - } + secretRef := "" + typedClient, err := s.clientGetter.Typed(ctx, s.kubeappsCluster) + if err != nil { + return "", err } - if _, ok := secret.Data["certFile"]; ok { - if _, ok = secret.Data["keyFile"]; ok { - auth.Type = corev1.PackageRepositoryAuth_PACKAGE_REPOSITORY_AUTH_TYPE_TLS - auth.PackageRepoAuthOneOf = &corev1.PackageRepositoryAuth_SecretRef{ - SecretRef: &corev1.SecretKeyReference{Name: secret.Name}, + secretInterface := typedClient.CoreV1().Secrets(repoName.Namespace) + if secret != nil { + if existingSecretRef == nil { + // create a secret first + newSecret, err := secretInterface.Create(ctx, secret, metav1.CreateOptions{}) + if err != nil { + return "", statuserror.FromK8sError("create", "secret", secret.GetGenerateName(), err) } - } - } else if _, ok := secret.Data["username"]; ok { - if _, ok = secret.Data["password"]; ok { - auth.Type = corev1.PackageRepositoryAuth_PACKAGE_REPOSITORY_AUTH_TYPE_BASIC_AUTH - auth.PackageRepoAuthOneOf = &corev1.PackageRepositoryAuth_SecretRef{ - SecretRef: &corev1.SecretKeyReference{Name: secret.Name}, + secretRef = newSecret.GetName() + } else { + // TODO (gfichtenholt) we should optimize this to somehow tell if the existing secret + // is the same (data-wise) as the new one and if so skip all this + if err = secretInterface.Delete(ctx, existingSecretRef.Name, metav1.DeleteOptions{}); err != nil { + return "", statuserror.FromK8sError("get", "secret", existingSecretRef.Name, err) } + // create a new one + newSecret, err := secretInterface.Create(ctx, secret, metav1.CreateOptions{}) + if err != nil { + return "", statuserror.FromK8sError("update", "secret", secret.GetGenerateName(), err) + } + secretRef = newSecret.GetName() } - } else if _, ok := secret.Data[".dockerconfigjson"]; ok { - auth.Type = corev1.PackageRepositoryAuth_PACKAGE_REPOSITORY_AUTH_TYPE_DOCKER_CONFIG_JSON - auth.PackageRepoAuthOneOf = &corev1.PackageRepositoryAuth_SecretRef{ - SecretRef: &corev1.SecretKeyReference{Name: secret.Name}, + } else if existingSecretRef != nil { + if err = secretInterface.Delete(ctx, existingSecretRef.Name, metav1.DeleteOptions{}); err != nil { + log.Errorf("Error deleting existing secret: [%s] due to %v", err) } - } else { - log.Warning("Unrecognized type of secret [%s]", secret.Name) } - return tlsConfig, auth, nil + return secretRef, nil } -func (s *Server) getRepoTlsConfigAndAuthWithKubeappsManagedSecrets(secret *apiv1.Secret) (*corev1.PackageRepositoryTlsConfig, *corev1.PackageRepositoryAuth, error) { - var tlsConfig *corev1.PackageRepositoryTlsConfig - auth := &corev1.PackageRepositoryAuth{ - Type: corev1.PackageRepositoryAuth_PACKAGE_REPOSITORY_AUTH_TYPE_UNSPECIFIED, +func (s *Server) updateRepo(ctx context.Context, repoRef *corev1.PackageRepositoryReference, url string, interval uint32, tlsConfig *corev1.PackageRepositoryTlsConfig, auth *corev1.PackageRepositoryAuth) (*corev1.PackageRepositoryReference, error) { + key := types.NamespacedName{Namespace: repoRef.GetContext().GetNamespace(), Name: repoRef.GetIdentifier()} + repo, err := s.getRepoInCluster(ctx, key) + if err != nil { + return nil, err } - if caFile, ok := secret.Data["caFile"]; ok { - tlsConfig = &corev1.PackageRepositoryTlsConfig{ - // flux plug in doesn't support this option - InsecureSkipVerify: false, - PackageRepoTlsConfigOneOf: &corev1.PackageRepositoryTlsConfig_CertAuthority{ - CertAuthority: string(caFile), - }, - } + // (gfichtenholt) per discussion will Michael 4/12/2022 + // for now: we disallow updates to pending repos and allow them for non-pending ones + // (i.e. success or failed status) + complete, _, _ := isHelmRepositoryReady(*repo) + if !complete { + return nil, status.Errorf(codes.Internal, "updates to repositories pending reconciliation are not supported") } - if certFile, ok := secret.Data["certFile"]; ok { - if keyFile, ok := secret.Data["keyFile"]; ok { - auth.Type = corev1.PackageRepositoryAuth_PACKAGE_REPOSITORY_AUTH_TYPE_TLS - auth.PackageRepoAuthOneOf = &corev1.PackageRepositoryAuth_TlsCertKey{ - TlsCertKey: &corev1.TlsCertKey{ - Cert: string(certFile), - Key: string(keyFile), - }, - } - } - } else if username, ok := secret.Data["username"]; ok { - if pwd, ok := secret.Data["password"]; ok { - auth.Type = corev1.PackageRepositoryAuth_PACKAGE_REPOSITORY_AUTH_TYPE_BASIC_AUTH - auth.PackageRepoAuthOneOf = &corev1.PackageRepositoryAuth_UsernamePassword{ - UsernamePassword: &corev1.UsernamePassword{ - Username: string(username), - Password: string(pwd), - }, - } - } - } else if configStr, ok := secret.Data[".dockerconfigjson"]; ok { - dc, err := common.SecretDataToDockerCredentials(string(configStr)) - if err != nil { - return nil, nil, err + if url == "" { + return nil, status.Errorf(codes.InvalidArgument, "repository url may not be empty") + } + repo.Spec.URL = url + + // flux does not grok description yet + + if interval > 0 { + repo.Spec.Interval = metav1.Duration{Duration: time.Duration(interval) * time.Second} + } else { + // interval is a required field + repo.Spec.Interval = defaultPollInterval + } + + if tlsConfig != nil && tlsConfig.InsecureSkipVerify { + return nil, status.Errorf(codes.InvalidArgument, "TLS flag insecureSkipVerify is not supported") + } + + var secretRef string + if s.pluginConfig.UserManagedSecrets { + if secretRef, err = s.validateRepoUserManagedSecrets(ctx, key, tlsConfig, auth); err != nil { + return nil, err } - auth.Type = corev1.PackageRepositoryAuth_PACKAGE_REPOSITORY_AUTH_TYPE_DOCKER_CONFIG_JSON - auth.PackageRepoAuthOneOf = &corev1.PackageRepositoryAuth_DockerCreds{ - DockerCreds: dc, + } else { + if secretRef, err = s.updateRepoKubeappsManagedSecrets(ctx, key, tlsConfig, auth, repo.Spec.SecretRef); err != nil { + return nil, err } + } + + if secretRef != "" { + repo.Spec.SecretRef = &fluxmeta.LocalObjectReference{Name: secretRef} } else { - log.Warning("Unrecognized type of secret [%s]", secret.Name) + repo.Spec.SecretRef = nil } - return tlsConfig, auth, nil + + repo.Spec.PassCredentials = auth != nil && auth.PassCredentials + + // get rid of the status field, since now there will be a new reconciliation + // process and the current status no longer applies. metadata and spec I want + // to keep, as they may have had added labels and/or annotations and/or + // even other changes made by the user. + repo.Status = sourcev1.HelmRepositoryStatus{} + + client, err := s.getClient(ctx, key.Namespace) + if err != nil { + return nil, err + } + if err = client.Update(ctx, repo); err != nil { + return nil, statuserror.FromK8sError("update", "HelmRepository", key.String(), err) + } + + log.V(4).Infof("Updated repository: %s", common.PrettyPrint(repo)) + + return &corev1.PackageRepositoryReference{ + Context: &corev1.Context{ + Namespace: key.Namespace, + Cluster: s.kubeappsCluster, + }, + Identifier: key.Name, + Plugin: GetPluginDetail(), + }, nil } // @@ -937,3 +891,188 @@ func checkRepoGeneration(repo sourcev1.HelmRepository) bool { observedGeneration := repo.Status.ObservedGeneration return generation > 0 && generation == observedGeneration } + +// ref https://fluxcd.io/docs/components/source/helmrepositories/ +func newFluxHelmRepo( + targetName types.NamespacedName, + url string, + interval uint32, + secretRef string, + passCredentials bool) (*sourcev1.HelmRepository, error) { + pollInterval := defaultPollInterval + if interval > 0 { + pollInterval = metav1.Duration{Duration: time.Duration(interval) * time.Second} + } + fluxRepo := &sourcev1.HelmRepository{ + TypeMeta: metav1.TypeMeta{ + Kind: sourcev1.HelmRepositoryKind, + APIVersion: sourcev1.GroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: targetName.Name, + Namespace: targetName.Namespace, + }, + Spec: sourcev1.HelmRepositorySpec{ + URL: url, + Interval: pollInterval, + }, + } + if secretRef != "" { + fluxRepo.Spec.SecretRef = &fluxmeta.LocalObjectReference{ + Name: secretRef, + } + } + if passCredentials { + fluxRepo.Spec.PassCredentials = true + } + return fluxRepo, nil +} + +// this func is only used with kubeapps-managed secrets +func newSecretFromTlsConfigAndAuth(repoName types.NamespacedName, + tlsConfig *corev1.PackageRepositoryTlsConfig, + auth *corev1.PackageRepositoryAuth) (*apiv1.Secret, error) { + var secret *apiv1.Secret + if tlsConfig != nil { + if tlsConfig.GetSecretRef() != nil { + return nil, status.Errorf(codes.InvalidArgument, "SecretRef may not be used with kubeapps managed secrets") + } + caCert := tlsConfig.GetCertAuthority() + if caCert != "" { + secret = common.NewLocalOpaqueSecret(repoName.Name + "-") + secret.Data["caFile"] = []byte(caCert) + } + } + if auth != nil { + if auth.GetSecretRef() != nil { + return nil, status.Errorf(codes.InvalidArgument, "SecretRef may not be used with kubeapps managed secrets") + } + if secret == nil { + secret = common.NewLocalOpaqueSecret(repoName.Name + "-") + } + switch auth.Type { + case corev1.PackageRepositoryAuth_PACKAGE_REPOSITORY_AUTH_TYPE_BASIC_AUTH: + if unp := auth.GetUsernamePassword(); unp != nil { + secret.Data["username"] = []byte(unp.Username) + secret.Data["password"] = []byte(unp.Password) + } else { + return nil, status.Errorf(codes.Internal, "Username/Password configuration is missing") + } + case corev1.PackageRepositoryAuth_PACKAGE_REPOSITORY_AUTH_TYPE_TLS: + if ck := auth.GetTlsCertKey(); ck != nil { + secret.Data["certFile"] = []byte(ck.Cert) + secret.Data["keyFile"] = []byte(ck.Key) + } else { + return nil, status.Errorf(codes.Internal, "TLS Cert/Key configuration is missing") + } + case corev1.PackageRepositoryAuth_PACKAGE_REPOSITORY_AUTH_TYPE_BEARER, corev1.PackageRepositoryAuth_PACKAGE_REPOSITORY_AUTH_TYPE_CUSTOM: + return nil, status.Errorf(codes.Unimplemented, "Package repository authentication type %q is not supported", auth.Type) + case corev1.PackageRepositoryAuth_PACKAGE_REPOSITORY_AUTH_TYPE_DOCKER_CONFIG_JSON: + if dc := auth.GetDockerCreds(); dc != nil { + secret.Type = apiv1.SecretTypeDockerConfigJson + secret.Data[".dockerconfigjson"] = common.DockerCredentialsToSecretData(dc) + } else { + return nil, status.Errorf(codes.Internal, "Docker credentials configuration is missing") + } + case corev1.PackageRepositoryAuth_PACKAGE_REPOSITORY_AUTH_TYPE_UNSPECIFIED: + return nil, nil + default: + return nil, status.Errorf(codes.Internal, "Unexpected package repository authentication type: %q", auth.Type) + } + } + return secret, nil +} + +func getRepoTlsConfigAndAuthWithUserManagedSecrets(secret *apiv1.Secret) (*corev1.PackageRepositoryTlsConfig, *corev1.PackageRepositoryAuth, error) { + var tlsConfig *corev1.PackageRepositoryTlsConfig + auth := &corev1.PackageRepositoryAuth{ + Type: corev1.PackageRepositoryAuth_PACKAGE_REPOSITORY_AUTH_TYPE_UNSPECIFIED, + } + + if _, ok := secret.Data["caFile"]; ok { + tlsConfig = &corev1.PackageRepositoryTlsConfig{ + // flux plug in doesn't support this option + InsecureSkipVerify: false, + PackageRepoTlsConfigOneOf: &corev1.PackageRepositoryTlsConfig_SecretRef{ + SecretRef: &corev1.SecretKeyReference{ + Name: secret.Name, + Key: "caFile", + }, + }, + } + } + if _, ok := secret.Data["certFile"]; ok { + if _, ok = secret.Data["keyFile"]; ok { + auth.Type = corev1.PackageRepositoryAuth_PACKAGE_REPOSITORY_AUTH_TYPE_TLS + auth.PackageRepoAuthOneOf = &corev1.PackageRepositoryAuth_SecretRef{ + SecretRef: &corev1.SecretKeyReference{Name: secret.Name}, + } + } + } else if _, ok := secret.Data["username"]; ok { + if _, ok = secret.Data["password"]; ok { + auth.Type = corev1.PackageRepositoryAuth_PACKAGE_REPOSITORY_AUTH_TYPE_BASIC_AUTH + auth.PackageRepoAuthOneOf = &corev1.PackageRepositoryAuth_SecretRef{ + SecretRef: &corev1.SecretKeyReference{Name: secret.Name}, + } + } + } else if _, ok := secret.Data[".dockerconfigjson"]; ok { + auth.Type = corev1.PackageRepositoryAuth_PACKAGE_REPOSITORY_AUTH_TYPE_DOCKER_CONFIG_JSON + auth.PackageRepoAuthOneOf = &corev1.PackageRepositoryAuth_SecretRef{ + SecretRef: &corev1.SecretKeyReference{Name: secret.Name}, + } + } else { + log.Warning("Unrecognized type of secret [%s]", secret.Name) + } + return tlsConfig, auth, nil +} + +func getRepoTlsConfigAndAuthWithKubeappsManagedSecrets(secret *apiv1.Secret) (*corev1.PackageRepositoryTlsConfig, *corev1.PackageRepositoryAuth, error) { + var tlsConfig *corev1.PackageRepositoryTlsConfig + auth := &corev1.PackageRepositoryAuth{ + Type: corev1.PackageRepositoryAuth_PACKAGE_REPOSITORY_AUTH_TYPE_UNSPECIFIED, + } + + if caFile, ok := secret.Data["caFile"]; ok { + tlsConfig = &corev1.PackageRepositoryTlsConfig{ + // flux plug in doesn't support this option + InsecureSkipVerify: false, + PackageRepoTlsConfigOneOf: &corev1.PackageRepositoryTlsConfig_CertAuthority{ + CertAuthority: string(caFile), + }, + } + } + + if certFile, ok := secret.Data["certFile"]; ok { + if keyFile, ok := secret.Data["keyFile"]; ok { + auth.Type = corev1.PackageRepositoryAuth_PACKAGE_REPOSITORY_AUTH_TYPE_TLS + auth.PackageRepoAuthOneOf = &corev1.PackageRepositoryAuth_TlsCertKey{ + TlsCertKey: &corev1.TlsCertKey{ + Cert: string(certFile), + Key: string(keyFile), + }, + } + } + } else if username, ok := secret.Data["username"]; ok { + if pwd, ok := secret.Data["password"]; ok { + auth.Type = corev1.PackageRepositoryAuth_PACKAGE_REPOSITORY_AUTH_TYPE_BASIC_AUTH + auth.PackageRepoAuthOneOf = &corev1.PackageRepositoryAuth_UsernamePassword{ + UsernamePassword: &corev1.UsernamePassword{ + Username: string(username), + Password: string(pwd), + }, + } + } + } else if configStr, ok := secret.Data[".dockerconfigjson"]; ok { + dc, err := common.SecretDataToDockerCredentials(string(configStr)) + if err != nil { + return nil, nil, err + } + auth.Type = corev1.PackageRepositoryAuth_PACKAGE_REPOSITORY_AUTH_TYPE_DOCKER_CONFIG_JSON + auth.PackageRepoAuthOneOf = &corev1.PackageRepositoryAuth_DockerCreds{ + DockerCreds: dc, + } + } else { + log.Warning("Unrecognized type of secret [%s]", secret.Name) + } + return tlsConfig, auth, nil +} diff --git a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/repo_integration_test.go b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/repo_integration_test.go index 3dca1f5ac33..e49520ad880 100644 --- a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/repo_integration_test.go +++ b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/repo_integration_test.go @@ -628,7 +628,10 @@ func TestKindClusterGetPackageRepositorySummaries(t *testing.T) { t.Fatal(err) } // want to wait until all repos reach Ready state - kubeWaitUntilHelmRepositoryIsReady(t, name, namespace) + err := kubeWaitUntilHelmRepositoryIsReady(t, name, namespace) + if err != nil { + t.Fatal(err) + } t.Cleanup(func() { if err = kubeDeleteHelmRepository(t, name, namespace); err != nil { t.Logf("Failed to delete helm source due to [%v]", err) @@ -669,3 +672,212 @@ func TestKindClusterGetPackageRepositorySummaries(t *testing.T) { }) } } + +func TestKindClusterUpdatePackageRepository(t *testing.T) { + _, fluxPluginReposClient, err := checkEnv(t) + if err != nil { + t.Fatal(err) + } + + testCases := []struct { + testName string + request *corev1.UpdatePackageRepositoryRequest + repoName string + repoUrl string + unauthorized bool + failed bool + expectedResponse *corev1.UpdatePackageRepositoryResponse + expectedDetail *corev1.GetPackageRepositoryDetailResponse + expectedStatusCode codes.Code + existingSecret *apiv1.Secret + userManagedSecrets bool + }{ + { + testName: "update url and auth for podinfo package repository", + request: update_repo_req_11, + repoName: "my-podinfo", + repoUrl: podinfo_repo_url, + expectedStatusCode: codes.OK, + expectedResponse: update_repo_resp_2, + expectedDetail: update_repo_detail_11, + }, + { + testName: "update package repository in a failed state", + request: update_repo_req_12, + repoName: "my-podinfo-2", + repoUrl: podinfo_basic_auth_repo_url, + expectedStatusCode: codes.OK, + expectedResponse: update_repo_resp_3, + expectedDetail: update_repo_detail_12, + failed: true, + }, + } + + adminAcctName := "test-update-repo-admin-" + randSeq(4) + grpcAdmin, err := newGrpcAdminContext(t, adminAcctName, "default") + if err != nil { + t.Fatal(err) + } + + loserAcctName := "test-update-repo-loser-" + randSeq(4) + grpcLoser, err := newGrpcContextForServiceAccountWithoutAccessToAnyNamespace(t, loserAcctName, "default") + if err != nil { + t.Fatal(err) + } + + for _, tc := range testCases { + t.Run(tc.testName, func(t *testing.T) { + repoNamespace := "test-" + randSeq(4) + + if err := kubeCreateNamespace(t, repoNamespace); err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if err := kubeDeleteNamespace(t, repoNamespace); err != nil { + t.Logf("Failed to delete namespace [%s] due to [%v]", repoNamespace, err) + } + }) + + secretName := "" + if tc.existingSecret != nil { + tc.existingSecret.Namespace = repoNamespace + if err := kubeCreateSecret(t, tc.existingSecret); err != nil { + t.Fatalf("%v", err) + } + secretName = tc.existingSecret.Name + t.Cleanup(func() { + err := kubeDeleteSecret(t, tc.existingSecret.Namespace, tc.existingSecret.Name) + if err != nil { + t.Logf("Failed to delete secret due to [%v]", err) + } + }) + } + + if err = kubeAddHelmRepository(t, tc.repoName, tc.repoUrl, repoNamespace, secretName, 0); err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if err = kubeDeleteHelmRepository(t, tc.repoName, repoNamespace); err != nil { + t.Logf("Failed to delete helm source due to [%v]", err) + } + }) + // wait until this repo reaches 'Ready' state so that long indexation process kicks in + err := kubeWaitUntilHelmRepositoryIsReady(t, tc.repoName, repoNamespace) + if err != nil { + if !tc.failed { + t.Fatalf("%v", err) + } else { + // sanity check : make sure repo is in failed state + if err.Error() != "Failed: failed to fetch Helm repository index: failed to cache index to temporary file: failed to fetch http://fluxv2plugin-testdata-svc.default.svc.cluster.local:80/podinfo-basic-auth/index.yaml : 401 Unauthorized" { + t.Fatalf("%v", err) + } + // TODO try and find a better way to wait until repo state stops changing to avoid + // rpc error: code = Internal desc = unable to update the HelmRepository + // 'test-nsrp/my-podinfo-2' due to 'Operation cannot be fulfilled on + // helmrepositories.source.toolkit.fluxcd.io "my-podinfo-2": the object has been modified; + // please apply your changes to the latest version and try again + t.Logf("Waiting 1s for repository [%s] to come to quiescent state", tc.repoName) + time.Sleep(1 * time.Second) + } + } + + var grpcCtx context.Context + var cancel context.CancelFunc + if tc.unauthorized { + grpcCtx, cancel = context.WithTimeout(grpcLoser, defaultContextTimeout) + } else { + grpcCtx, cancel = context.WithTimeout(grpcAdmin, defaultContextTimeout) + } + defer cancel() + + oldValue, err := fluxPluginReposClient.SetUserManagedSecrets( + grpcCtx, &v1alpha1.SetUserManagedSecretsRequest{Value: tc.userManagedSecrets}) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + fluxPluginReposClient.SetUserManagedSecrets( + grpcCtx, &v1alpha1.SetUserManagedSecretsRequest{Value: oldValue.Value}) + }) + + tc.request.PackageRepoRef.Context.Namespace = repoNamespace + if tc.expectedResponse != nil { + tc.expectedResponse.PackageRepoRef.Context.Namespace = repoNamespace + } + if tc.expectedDetail != nil { + tc.expectedDetail.Detail.PackageRepoRef.Context.Namespace = repoNamespace + } + + resp, err := fluxPluginReposClient.UpdatePackageRepository(grpcCtx, tc.request) + if got, want := status.Code(err), tc.expectedStatusCode; got != want { + t.Fatalf("got: %v, want: %v", err, want) + } + if tc.expectedStatusCode != codes.OK { + // we are done + return + } + + opts := cmpopts.IgnoreUnexported( + corev1.Context{}, + corev1.PackageRepositoryReference{}, + plugins.Plugin{}, + corev1.UpdatePackageRepositoryResponse{}, + ) + + if got, want := resp, tc.expectedResponse; !cmp.Equal(want, got, opts) { + t.Errorf("mismatch (-want +got):\n%s", cmp.Diff(want, got, opts, opts)) + } + + var actualDetail *corev1.GetPackageRepositoryDetailResponse + for i := 0; i < 10; i++ { + actualDetail, err = fluxPluginReposClient.GetPackageRepositoryDetail( + grpcCtx, + &corev1.GetPackageRepositoryDetailRequest{ + PackageRepoRef: &corev1.PackageRepositoryReference{ + Context: &corev1.Context{ + Namespace: repoNamespace, + }, + Identifier: tc.repoName, + }, + }) + if got, want := status.Code(err), codes.OK; got != want { + t.Fatalf("got: %v, want: %v", err, want) + } + if actualDetail.Detail.Status.Reason == corev1.PackageRepositoryStatus_STATUS_REASON_SUCCESS { + break + } else { + t.Logf("Waiting 2s for repository reconciliation to complete successfully...") + time.Sleep(2 * time.Second) + } + } + if actualDetail.Detail.Status.Reason != corev1.PackageRepositoryStatus_STATUS_REASON_SUCCESS { + t.Fatalf("Timed out waiting for repository [%q] reconcile successfully after the update", tc.repoName) + } + + opts1 := cmpopts.IgnoreUnexported( + corev1.Context{}, + corev1.PackageRepositoryReference{}, + plugins.Plugin{}, + corev1.GetPackageRepositoryDetailResponse{}, + corev1.PackageRepositoryDetail{}, + corev1.PackageRepositoryStatus{}, + corev1.PackageRepositoryAuth{}, + corev1.PackageRepositoryTlsConfig{}, + corev1.SecretKeyReference{}, + corev1.UsernamePassword{}, + ) + + opts2 := cmpopts.IgnoreFields(corev1.PackageRepositoryStatus{}, "UserReason") + + if got, want := actualDetail, tc.expectedDetail; !cmp.Equal(want, got, opts1, opts2) { + t.Fatalf("mismatch (-want +got):\n%s", cmp.Diff(want, got, opts, opts1, opts2)) + } + + if !strings.HasPrefix(actualDetail.GetDetail().Status.UserReason, tc.expectedDetail.Detail.Status.UserReason) { + t.Errorf("unexpected response (status.UserReason): (-want +got):\n- %s\n+ %s", + tc.expectedDetail.Detail.Status.UserReason, + actualDetail.GetDetail().Status.UserReason) + } + }) + } +} diff --git a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/repo_test.go b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/repo_test.go index 752f717c356..085a08519a5 100644 --- a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/repo_test.go +++ b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/repo_test.go @@ -1324,6 +1324,13 @@ func TestAddPackageRepository(t *testing.T) { statusCode: codes.InvalidArgument, userManagedSecrets: true, }, + { + name: "package repository with just pass_credentials flag", + request: add_repo_req_20, + expectedResponse: add_repo_expected_resp, + expectedRepo: &add_repo_5, + statusCode: codes.OK, + }, } for _, tc := range testCases { @@ -1389,28 +1396,41 @@ func TestAddPackageRepository(t *testing.T) { if err = ctrlClient.Get(ctx, nsname, &actualRepo); err != nil { t.Fatal(err) } else { - opt1 := cmpopts.IgnoreFields(sourcev1.HelmRepositorySpec{}, "SecretRef") - - if got, want := &actualRepo, tc.expectedRepo; !cmp.Equal(want, got, opt1) { - t.Errorf("mismatch (-want +got):\n%s", cmp.Diff(want, got, opt1)) - } + if tc.userManagedSecrets { + if tc.expectedCreatedSecret != nil { + t.Fatalf("Error: unexpected state") + } + if got, want := &actualRepo, tc.expectedRepo; !cmp.Equal(want, got) { + t.Errorf("mismatch (-want +got):\n%s", cmp.Diff(want, got)) + } + } else { + opt1 := cmpopts.IgnoreFields(sourcev1.HelmRepositorySpec{}, "SecretRef") - if tc.expectedCreatedSecret != nil { - if !strings.HasPrefix(actualRepo.Spec.SecretRef.Name, tc.expectedRepo.Spec.SecretRef.Name) { - t.Errorf("SecretRef [%s] was expected to start with [%s]", - actualRepo.Spec.SecretRef.Name, tc.expectedRepo.Spec.SecretRef.Name) + if got, want := &actualRepo, tc.expectedRepo; !cmp.Equal(want, got, opt1) { + t.Errorf("mismatch (-want +got):\n%s", cmp.Diff(want, got, opt1)) } - opt2 := cmpopts.IgnoreFields(metav1.ObjectMeta{}, "Name", "GenerateName") - // check expected secret has been created - if typedClient, err := s.clientGetter.Typed(ctx, s.kubeappsCluster); err != nil { - t.Fatal(err) - } else if secret, err := typedClient.CoreV1().Secrets(nsname.Namespace).Get(ctx, actualRepo.Spec.SecretRef.Name, metav1.GetOptions{}); err != nil { - t.Fatal(err) - } else if got, want := secret, tc.expectedCreatedSecret; !cmp.Equal(want, got, opt2) { - t.Errorf("mismatch (-want +got):\n%s", cmp.Diff(want, got, opt2)) - } else if !strings.HasPrefix(secret.Name, tc.expectedCreatedSecret.Name) { - t.Errorf("Secret Name [%s] was expected to start with [%s]", - secret.Name, tc.expectedCreatedSecret.Name) + + if tc.expectedCreatedSecret != nil { + if !strings.HasPrefix(actualRepo.Spec.SecretRef.Name, tc.expectedRepo.Spec.SecretRef.Name) { + t.Errorf("SecretRef [%s] was expected to start with [%s]", + actualRepo.Spec.SecretRef.Name, tc.expectedRepo.Spec.SecretRef.Name) + } + opt2 := cmpopts.IgnoreFields(metav1.ObjectMeta{}, "Name", "GenerateName") + // check expected secret has been created + if typedClient, err := s.clientGetter.Typed(ctx, s.kubeappsCluster); err != nil { + t.Fatal(err) + } else if secret, err := typedClient.CoreV1().Secrets(nsname.Namespace).Get(ctx, actualRepo.Spec.SecretRef.Name, metav1.GetOptions{}); err != nil { + t.Fatal(err) + } else if got, want := secret, tc.expectedCreatedSecret; !cmp.Equal(want, got, opt2) { + t.Errorf("mismatch (-want +got):\n%s", cmp.Diff(want, got, opt2)) + } else if !strings.HasPrefix(secret.Name, tc.expectedCreatedSecret.Name) { + t.Errorf("Secret Name [%s] was expected to start with [%s]", + secret.Name, tc.expectedCreatedSecret.Name) + } + } else if actualRepo.Spec.SecretRef != nil { + t.Fatalf("Expected no secret, but found: [%q]", actualRepo.Spec.SecretRef.Name) + } else if tc.expectedRepo.Spec.SecretRef != nil { + t.Fatalf("Error: unexpected state") } } } @@ -1754,6 +1774,257 @@ func TestGetPackageRepositorySummaries(t *testing.T) { } } +func TestUpdatePackageRepository(t *testing.T) { + ca, pub, priv := getCertsForTesting(t) + testCases := []struct { + name string + request *corev1.UpdatePackageRepositoryRequest + repoIndex string + repoName string + repoNamespace string + oldRepoSecret *apiv1.Secret + newRepoSecret *apiv1.Secret + pending bool + expectedStatusCode codes.Code + expectedResponse *corev1.UpdatePackageRepositoryResponse + expectedDetail *corev1.GetPackageRepositoryDetailResponse + userManagedSecrets bool + }{ + { + name: "update repository url", + repoIndex: testYaml("valid-index.yaml"), + repoName: "repo-1", + repoNamespace: "namespace-1", + request: update_repo_req_1, + expectedStatusCode: codes.OK, + expectedResponse: update_repo_resp_1, + expectedDetail: update_repo_detail_1, + }, + { + name: "update repository poll interval", + repoIndex: testYaml("valid-index.yaml"), + repoName: "repo-1", + repoNamespace: "namespace-1", + request: update_repo_req_2, + expectedStatusCode: codes.OK, + expectedResponse: update_repo_resp_1, + expectedDetail: update_repo_detail_2, + }, + { + name: "update repository pass credentials flag", + repoIndex: testYaml("valid-index.yaml"), + repoName: "repo-1", + repoNamespace: "namespace-1", + request: update_repo_req_3, + expectedStatusCode: codes.OK, + expectedResponse: update_repo_resp_1, + expectedDetail: update_repo_detail_3, + }, + { + name: "update repository set TLS cert authority", + repoIndex: testYaml("valid-index.yaml"), + repoName: "repo-1", + repoNamespace: "namespace-1", + newRepoSecret: newTlsSecret("secret-1", "namespace-1", nil, nil, ca), + request: update_repo_req_4, + expectedStatusCode: codes.OK, + expectedResponse: update_repo_resp_1, + expectedDetail: update_repo_detail_4, + userManagedSecrets: true, + }, + { + name: "update repository unset TLS cert authority", + repoIndex: testYaml("valid-index.yaml"), + repoName: "repo-1", + repoNamespace: "namespace-1", + oldRepoSecret: newTlsSecret("secret-1", "namespace-1", nil, nil, ca), + request: update_repo_req_5, + expectedStatusCode: codes.OK, + expectedResponse: update_repo_resp_1, + expectedDetail: update_repo_detail_5, + userManagedSecrets: true, + }, + { + name: "update repository set basic auth", + repoIndex: testYaml("valid-index.yaml"), + repoName: "repo-1", + repoNamespace: "namespace-1", + newRepoSecret: newBasicAuthSecret("secret-1", "namespace-1", "foo", "bar"), + request: update_repo_req_6, + expectedStatusCode: codes.OK, + expectedResponse: update_repo_resp_1, + expectedDetail: update_repo_detail_6, + userManagedSecrets: true, + }, + { + name: "update repository unset basic auth", + repoIndex: testYaml("valid-index.yaml"), + repoName: "repo-1", + repoNamespace: "namespace-1", + oldRepoSecret: newBasicAuthSecret("secret-1", "namespace-1", "foo", "bar"), + request: update_repo_req_7, + expectedStatusCode: codes.OK, + expectedResponse: update_repo_resp_1, + expectedDetail: update_repo_detail_7, + userManagedSecrets: true, + }, + { + name: "update repository set TLS cert/key (kubeapps-managed secrets)", + repoIndex: testYaml("valid-index.yaml"), + repoName: "repo-1", + repoNamespace: "namespace-1", + request: update_repo_req_8(pub, priv), + expectedStatusCode: codes.OK, + expectedResponse: update_repo_resp_1, + expectedDetail: update_repo_detail_8(pub, priv), + }, + { + name: "update repository unset TLS cert/key (kubeapps-managed secrets)", + repoIndex: testYaml("valid-index.yaml"), + repoName: "repo-1", + repoNamespace: "namespace-1", + oldRepoSecret: newTlsSecret("secret-1", "namespace-1", pub, priv, nil), + request: update_repo_req_9, + expectedStatusCode: codes.OK, + expectedResponse: update_repo_resp_1, + expectedDetail: update_repo_detail_9, + }, + { + name: "update repository change from TLS cert/key to basic auth (kubeapps-managed secrets)", + repoIndex: testYaml("valid-index.yaml"), + repoName: "repo-1", + repoNamespace: "namespace-1", + oldRepoSecret: newTlsSecret("secret-1", "namespace-1", pub, priv, nil), + request: update_repo_req_10, + expectedStatusCode: codes.OK, + expectedResponse: update_repo_resp_1, + expectedDetail: update_repo_detail_10, + }, + { + name: "updates to pending repo is not allowed", + repoIndex: testYaml("valid-index.yaml"), + repoName: "repo-1", + repoNamespace: "namespace-1", + request: update_repo_req_1, + expectedStatusCode: codes.Internal, + pending: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + oldSecretRef := "" + secrets := []runtime.Object{} + if tc.oldRepoSecret != nil { + oldSecretRef = tc.oldRepoSecret.Name + secrets = append(secrets, tc.oldRepoSecret) + } + if tc.newRepoSecret != nil { + secrets = append(secrets, tc.newRepoSecret) + } + var repo *sourcev1.HelmRepository + if !tc.pending { + var ts *httptest.Server + var err error + ts, repo, err = newRepoWithIndex(tc.repoIndex, tc.repoName, tc.repoNamespace, nil, oldSecretRef) + if err != nil { + t.Fatalf("%+v", err) + } + defer ts.Close() + } else { + repoSpec := &sourcev1.HelmRepositorySpec{ + URL: "https://example.repo.com/charts", + Interval: metav1.Duration{Duration: 1 * time.Minute}, + } + repoStatus := &sourcev1.HelmRepositoryStatus{ + Conditions: []metav1.Condition{ + { + LastTransitionTime: metav1.Time{Time: lastTransitionTime}, + Type: fluxmeta.ReadyCondition, + Status: metav1.ConditionUnknown, + Reason: fluxmeta.ProgressingReason, + Message: "reconciliation in progress", + }, + }, + } + repo1 := newRepo(tc.repoName, tc.repoNamespace, repoSpec, repoStatus) + repo = &repo1 + } + // update to the repo in a failed state will be tested in integration test + + // the index.yaml will contain links to charts but for the purposes + // of this test they do not matter + s, _, err := newServerWithRepos(t, []sourcev1.HelmRepository{*repo}, nil, secrets) + if err != nil { + t.Fatalf("error instantiating the server: %v", err) + } + s.pluginConfig.UserManagedSecrets = tc.userManagedSecrets + + ctx := context.Background() + actualResp, err := s.UpdatePackageRepository(ctx, tc.request) + if got, want := status.Code(err), tc.expectedStatusCode; got != want { + t.Fatalf("got: %+v, want: %+v, err: %+v", got, want, err) + } + + if tc.expectedStatusCode == codes.OK { + if actualResp == nil { + t.Fatalf("got: nil, want: response") + } else { + opt1 := cmpopts.IgnoreUnexported( + corev1.Context{}, + corev1.PackageRepositoryReference{}, + plugins.Plugin{}, + corev1.UpdatePackageRepositoryResponse{}, + ) + if got, want := actualResp, tc.expectedResponse; !cmp.Equal(got, want, opt1) { + t.Errorf("mismatch (-want +got):\n%s", cmp.Diff(want, got, opt1)) + } + } + } else { + // We don't need to check anything else for non-OK codes. + return + } + + actualDetail, err := s.GetPackageRepositoryDetail(ctx, &corev1.GetPackageRepositoryDetailRequest{ + PackageRepoRef: actualResp.PackageRepoRef, + }) + if got, want := status.Code(err), codes.OK; got != want { + t.Fatalf("got: %+v, want: %+v, err: %+v", got, want, err) + } + + if actualDetail == nil { + t.Fatalf("got: nil, want: detail") + } else { + opt1 := cmpopts.IgnoreUnexported( + corev1.Context{}, + corev1.PackageRepositoryReference{}, + plugins.Plugin{}, + corev1.GetPackageRepositoryDetailResponse{}, + corev1.PackageRepositoryDetail{}, + corev1.PackageRepositoryStatus{}, + corev1.PackageRepositoryAuth{}, + corev1.PackageRepositoryTlsConfig{}, + corev1.SecretKeyReference{}, + corev1.TlsCertKey{}, + corev1.UsernamePassword{}, + ) + if got, want := actualDetail, tc.expectedDetail; !cmp.Equal(got, want, opt1) { + t.Errorf("mismatch (-want +got):\n%s", cmp.Diff(want, got, opt1)) + } + } + + if !tc.userManagedSecrets && tc.oldRepoSecret != nil && actualDetail.Detail.Auth.Type == corev1.PackageRepositoryAuth_PACKAGE_REPOSITORY_AUTH_TYPE_UNSPECIFIED { + // check the secret's been deleted + if typedClient, err := s.clientGetter.Typed(ctx, s.kubeappsCluster); err != nil { + t.Fatal(err) + } else if _, err = typedClient.CoreV1().Secrets(tc.repoNamespace).Get(ctx, tc.oldRepoSecret.Name, metav1.GetOptions{}); err == nil { + t.Fatalf("Expected secret [%q] to have been deleted", tc.oldRepoSecret.Name) + } + } + }) + } +} + func newRepo(name string, namespace string, spec *sourcev1.HelmRepositorySpec, status *sourcev1.HelmRepositoryStatus) sourcev1.HelmRepository { helmRepository := sourcev1.HelmRepository{ TypeMeta: metav1.TypeMeta{ diff --git a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/server.go b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/server.go index a69c1a2e22f..7a7915e48e3 100644 --- a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/server.go +++ b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/server.go @@ -586,8 +586,27 @@ func (s *Server) GetPackageRepositorySummaries(ctx context.Context, request *cor // UpdatePackageRepository updates a package repository based on the request. func (s *Server) UpdatePackageRepository(ctx context.Context, request *corev1.UpdatePackageRepositoryRequest) (*corev1.UpdatePackageRepositoryResponse, error) { - // just a stub for now - return nil, nil + log.Infof("+fluxv2 UpdatePackageRepository [%v]", request) + if request == nil || request.PackageRepoRef == nil { + return nil, status.Errorf(codes.InvalidArgument, "no request PackageRepoRef provided") + } + + repoRef := request.PackageRepoRef + cluster := repoRef.GetContext().GetCluster() + if cluster != "" && cluster != s.kubeappsCluster { + return nil, status.Errorf( + codes.Unimplemented, + "not supported yet: request.packageRepoRef.Context.Cluster: [%v]", + cluster) + } + + if responseRef, err := s.updateRepo(ctx, repoRef, request.Url, request.Interval, request.TlsConfig, request.Auth); err != nil { + return nil, err + } else { + return &corev1.UpdatePackageRepositoryResponse{ + PackageRepoRef: responseRef, + }, nil + } } // This endpoint exists only for integration unit tests