From c6369bb360292b203b0608da7a95351fab810777 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Gr=C3=A4ff?= Date: Mon, 11 Nov 2024 15:47:32 +0100 Subject: [PATCH] Self targets (run-int-tests) (#1254) --- .../targettypes/kubernetes_cluster.go | 16 +- .../targettypes/kubernetes_cluster_test.go | 31 ++++ .../{ => 01-kubeconfig-targets}/README.md | 2 +- .../resources/clusterrolebinding.yaml.tpl | 0 .../targets/02-self-targets/README.md | 68 +++++++ .../commands/delete-installation.sh | 17 ++ .../commands/delete-other-k8s-resources.sh | 23 +++ .../commands/deploy-k8s-resources.sh | 45 +++++ .../targets/02-self-targets/commands/settings | 5 + .../02-self-targets/images/self-targets.png | Bin 0 -> 96452 bytes .../installation/clusterrolebinding.yaml.tpl | 12 ++ .../installation/installation.yaml.tpl | 53 ++++++ .../installation/serviceaccount.yaml.tpl | 5 + .../installation/target.yaml.tpl | 12 ++ docs/usage/Targets.md | 64 ++++++- pkg/deployer/helm/add.go | 6 +- pkg/deployer/helm/deletionmanager_test.go | 10 +- pkg/deployer/helm/deployer.go | 15 +- pkg/deployer/helm/ensure.go | 43 +++-- pkg/deployer/helm/helm.go | 45 ++--- pkg/deployer/helm/helm_suite_test.go | 2 +- .../realhelmdeployer/real_helm_deployer.go | 10 +- pkg/deployer/helm/test/e2e_test.go | 8 +- pkg/deployer/lib/controller.go | 9 +- .../readinesscheck/customreadinesscheck.go | 5 +- .../lib/resourcemanager/deletiongroup.go | 7 +- .../lib/resourcemanager/deletionmanager.go | 4 +- pkg/deployer/lib/resourcemanager/exporter.go | 6 +- .../lib/resourcemanager/objectapplier.go | 5 + pkg/deployer/lib/target_access.go | 168 ++++++++++++++++++ pkg/deployer/lib/target_client_provider.go | 14 +- pkg/deployer/lib/utils.go | 95 ---------- pkg/deployer/manifest/add.go | 2 +- pkg/deployer/manifest/controller.go | 14 +- pkg/deployer/manifest/ensure.go | 31 ++-- pkg/deployer/manifest/manifest.go | 39 ++-- pkg/deployer/manifest/test/e2e_test.go | 4 +- pkg/deployer/mock/add.go | 3 +- test/integration/suite_test.go | 13 +- test/integration/targets/oidc_targets.go | 159 ++++------------- test/integration/targets/register.go | 1 + test/integration/targets/self_targets.go | 81 +++++++++ .../oidc-targets/clusterrolebinding.yaml | 12 ++ .../oidc-targets/import-do-namespace.yaml | 6 - .../targets/oidc-targets/installation.yaml | 22 +-- .../targets/oidc-targets/openidconnect.yaml | 11 ++ .../targets/oidc-targets/serviceaccount.yaml | 5 + .../testdata/targets/oidc-targets/target.yaml | 16 ++ .../self-targets/clusterrolebinding.yaml | 12 ++ .../targets/self-targets/installation.yaml | 45 +++++ .../targets/self-targets/serviceaccount.yaml | 5 + .../testdata/targets/self-targets/target.yaml | 12 ++ test/utils/builder.go | 51 ++++++ 53 files changed, 956 insertions(+), 393 deletions(-) rename docs/guided-tour/targets/{ => 01-kubeconfig-targets}/README.md (97%) rename docs/guided-tour/targets/{ => 01-kubeconfig-targets}/resources/clusterrolebinding.yaml.tpl (100%) create mode 100644 docs/guided-tour/targets/02-self-targets/README.md create mode 100755 docs/guided-tour/targets/02-self-targets/commands/delete-installation.sh create mode 100755 docs/guided-tour/targets/02-self-targets/commands/delete-other-k8s-resources.sh create mode 100755 docs/guided-tour/targets/02-self-targets/commands/deploy-k8s-resources.sh create mode 100644 docs/guided-tour/targets/02-self-targets/commands/settings create mode 100644 docs/guided-tour/targets/02-self-targets/images/self-targets.png create mode 100644 docs/guided-tour/targets/02-self-targets/installation/clusterrolebinding.yaml.tpl create mode 100644 docs/guided-tour/targets/02-self-targets/installation/installation.yaml.tpl create mode 100644 docs/guided-tour/targets/02-self-targets/installation/serviceaccount.yaml.tpl create mode 100644 docs/guided-tour/targets/02-self-targets/installation/target.yaml.tpl create mode 100644 pkg/deployer/lib/target_access.go create mode 100644 test/integration/targets/self_targets.go create mode 100644 test/integration/testdata/targets/oidc-targets/clusterrolebinding.yaml delete mode 100644 test/integration/testdata/targets/oidc-targets/import-do-namespace.yaml create mode 100644 test/integration/testdata/targets/oidc-targets/openidconnect.yaml create mode 100644 test/integration/testdata/targets/oidc-targets/serviceaccount.yaml create mode 100644 test/integration/testdata/targets/oidc-targets/target.yaml create mode 100644 test/integration/testdata/targets/self-targets/clusterrolebinding.yaml create mode 100644 test/integration/testdata/targets/self-targets/installation.yaml create mode 100644 test/integration/testdata/targets/self-targets/serviceaccount.yaml create mode 100644 test/integration/testdata/targets/self-targets/target.yaml diff --git a/apis/core/v1alpha1/targettypes/kubernetes_cluster.go b/apis/core/v1alpha1/targettypes/kubernetes_cluster.go index 872c892d1..69ec7ed57 100644 --- a/apis/core/v1alpha1/targettypes/kubernetes_cluster.go +++ b/apis/core/v1alpha1/targettypes/kubernetes_cluster.go @@ -23,6 +23,9 @@ type KubernetesClusterTargetConfig struct { Kubeconfig ValueRef `json:"kubeconfig"` OIDCConfig *OIDCConfig `json:"oidcConfig,omitempty"` + + // SelfConfig contains the config for a Target that points to the landscaper resource cluster. + SelfConfig *SelfConfig `json:"selfConfig,omitempty"` } // DefaultKubeconfigKey is the default that is used to hold a kubeconfig. @@ -37,6 +40,7 @@ type ValueRef struct { type kubeconfigJSON struct { Kubeconfig *ValueRef `json:"kubeconfig"` OIDCConfig *OIDCConfig `json:"oidcConfig,omitempty"` + SelfConfig *SelfConfig `json:"selfConfig,omitempty"` } // MarshalJSON implements the json marshaling for a JSON @@ -60,12 +64,15 @@ func (v *ValueRef) UnmarshalJSON(data []byte) error { func (kc *KubernetesClusterTargetConfig) UnmarshalJSON(data []byte) error { kj := &kubeconfigJSON{} err := json.Unmarshal(data, kj) - if err == nil && (kj.Kubeconfig != nil || kj.OIDCConfig != nil) { + if err == nil && (kj.Kubeconfig != nil || kj.OIDCConfig != nil || kj.SelfConfig != nil) { // parsing was successful if kj.Kubeconfig != nil { kc.Kubeconfig = *kj.Kubeconfig } - kc.OIDCConfig = kj.OIDCConfig + if kj.OIDCConfig != nil { + kc.OIDCConfig = kj.OIDCConfig + } + kc.SelfConfig = kj.SelfConfig return nil } return kc.Kubeconfig.UnmarshalJSON(data) @@ -87,3 +94,8 @@ type OIDCConfig struct { Audience []string `json:"audience,omitempty"` ExpirationSeconds *int64 `json:"expirationSeconds,omitempty"` } + +type SelfConfig struct { + ServiceAccount v1.LocalObjectReference `json:"serviceAccount,omitempty"` + ExpirationSeconds *int64 `json:"expirationSeconds,omitempty"` +} diff --git a/apis/core/v1alpha1/targettypes/kubernetes_cluster_test.go b/apis/core/v1alpha1/targettypes/kubernetes_cluster_test.go index ed8327c74..51f4baf2b 100644 --- a/apis/core/v1alpha1/targettypes/kubernetes_cluster_test.go +++ b/apis/core/v1alpha1/targettypes/kubernetes_cluster_test.go @@ -75,4 +75,35 @@ var _ = Describe("Kubernetes Cluster Target Types", func() { }, })) }) + + It("should marshal a self config", func() { + targetConfig := &targettypes.KubernetesClusterTargetConfig{ + SelfConfig: &targettypes.SelfConfig{ + ServiceAccount: v1.LocalObjectReference{ + Name: "test-account", + }, + ExpirationSeconds: ptr.To[int64](300), + }, + } + targetConfigJSON, err := json.Marshal(targetConfig) + Expect(err).NotTo(HaveOccurred()) + Expect(targetConfigJSON).To(MatchJSON(`{"kubeconfig":null,"selfConfig":{"serviceAccount":{"name":"test-account"},"expirationSeconds":300}}`)) + }) + + It("should unmarshal a self config", func() { + configJSON := []byte(`{"selfConfig":{"serviceAccount":{"name":"test-account"},"expirationSeconds":300}}`) + config := &targettypes.KubernetesClusterTargetConfig{} + Expect(json.Unmarshal(configJSON, config)).To(Succeed()) + Expect(config).To(Equal(&targettypes.KubernetesClusterTargetConfig{ + Kubeconfig: targettypes.ValueRef{ + StrVal: nil, + }, + SelfConfig: &targettypes.SelfConfig{ + ServiceAccount: v1.LocalObjectReference{ + Name: "test-account", + }, + ExpirationSeconds: ptr.To[int64](300), + }, + })) + }) }) diff --git a/docs/guided-tour/targets/README.md b/docs/guided-tour/targets/01-kubeconfig-targets/README.md similarity index 97% rename from docs/guided-tour/targets/README.md rename to docs/guided-tour/targets/01-kubeconfig-targets/README.md index 9ac0dbd7d..262b9a3dd 100644 --- a/docs/guided-tour/targets/README.md +++ b/docs/guided-tour/targets/01-kubeconfig-targets/README.md @@ -13,7 +13,7 @@ If your target cluster is a Gardener shoot cluster, you typically have a for the target cluster. It is **not** possible to use such a kubeconfig in a `Target` custom resource. You have the following alternatives: -- Use an [OIDC Target](../../usage/Targets.md#oidc-target-to-kubernetes-target-cluster). +- Use an [OIDC Target](../../../usage/Targets.md#oidc-target-to-kubernetes-target-cluster) - Use a Target whose kubeconfig is based on a ServiceAccount token, as described below. ## Targets Whose Kubeconfig is Based On a ServiceAccount Token diff --git a/docs/guided-tour/targets/resources/clusterrolebinding.yaml.tpl b/docs/guided-tour/targets/01-kubeconfig-targets/resources/clusterrolebinding.yaml.tpl similarity index 100% rename from docs/guided-tour/targets/resources/clusterrolebinding.yaml.tpl rename to docs/guided-tour/targets/01-kubeconfig-targets/resources/clusterrolebinding.yaml.tpl diff --git a/docs/guided-tour/targets/02-self-targets/README.md b/docs/guided-tour/targets/02-self-targets/README.md new file mode 100644 index 000000000..b2d5f2c3e --- /dev/null +++ b/docs/guided-tour/targets/02-self-targets/README.md @@ -0,0 +1,68 @@ +--- +title: Self Targets +sidebar_position: 2 +--- + +# Self Targets + +This example demonstrates how you can use the Landscaper to deploy objects on its own resource cluster. +This means in this example the resource cluster and the target cluster are the same. +For this use-case, the Landscaper provides a special type of targets, so-called +[Self Targets](../../../usage/Targets.md#targets-to-the-landscaper-resource-cluster-self-targets). +Their advantage is that you do not need to include a kubeconfig into them. Instead, the Target references a ServiceAccount +in the same Namespace. The Self Target in this example looks as follows: + +```yaml +apiVersion: landscaper.gardener.cloud/v1alpha1 +kind: Target +metadata: + name: self-target + namespace: cu-example +spec: + type: landscaper.gardener.cloud/kubernetes-cluster + config: + selfConfig: + serviceAccount: + name: self-serviceaccount + expirationSeconds: 3600 +``` + +This Target references a [ServiceAccount `self-serviceaccount`](installation/serviceaccount.yaml.tpl). +A [ClusterRoleBinding `landscaper:guided-tour:self`](installation/clusterrolebinding.yaml.tpl) binds the ServiceAccount +to the ClusterRole `cluster-admin`, so that it has the necessary rights to create objects on the resource cluster. +The [Installation `self-inst`](installation/installation.yaml.tpl) uses the Target to deploy a ConfigMap on the +resource cluster. + + +## Procedure + +1. In the [settings](commands/settings) file, adjust the variables `RESOURCE_CLUSTER_KUBECONFIG_PATH`. + +2. On the Landscaper resource cluster, create namespaces `cu-example` and `example`. + +3. Run script [commands/deploy-k8s-resources.sh](commands/deploy-k8s-resources.sh). + It templates the following objects and applies them to the resource cluster: + - [ServiceAccount `self-serviceaccount`](installation/serviceaccount.yaml.tpl), + - [ClusterRoleBinding `landscaper:guided-tour:self`](installation/clusterrolebinding.yaml.tpl), + - [Target `self-target`](installation/target.yaml.tpl), + - [Installation `self-inst`](installation/installation.yaml.tpl). + + The diagram below provides an overview of these objects. + +4. Wait until the Installation is in phase `Succeeded` and check that it has created a ConfigMap `self-target-example` + in namespace `example` on the resource cluster. + +![diagram](./images/self-targets.png) + + +## Cleanup + +You can remove the Installation with the +[delete-installation script](commands/delete-installation.sh). +When the Installation is gone, you can delete the Target, ClusterRoleBinding, and ServiceAccount with the +[delete-other-k8s-resources script](commands/delete-other-k8s-resources.sh). + + +## References + +[Self Targets](../../../usage/Targets.md#targets-to-the-landscaper-resource-cluster-self-targets) diff --git a/docs/guided-tour/targets/02-self-targets/commands/delete-installation.sh b/docs/guided-tour/targets/02-self-targets/commands/delete-installation.sh new file mode 100755 index 000000000..1475ccb73 --- /dev/null +++ b/docs/guided-tour/targets/02-self-targets/commands/delete-installation.sh @@ -0,0 +1,17 @@ +#!/bin/bash +# +# SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors +# +# SPDX-License-Identifier: Apache-2.0 + +set -o errexit + +COMPONENT_DIR="$(dirname $0)/.." +cd "${COMPONENT_DIR}" +COMPONENT_DIR="$(pwd)" +echo "COMPONENT_DIR: ${COMPONENT_DIR}" + +source "${COMPONENT_DIR}/commands/settings" + +echo "deleting installation" +kubectl delete installation "self-inst" -n "${NAMESPACE}" --kubeconfig="${RESOURCE_CLUSTER_KUBECONFIG_PATH}" diff --git a/docs/guided-tour/targets/02-self-targets/commands/delete-other-k8s-resources.sh b/docs/guided-tour/targets/02-self-targets/commands/delete-other-k8s-resources.sh new file mode 100755 index 000000000..a56d563aa --- /dev/null +++ b/docs/guided-tour/targets/02-self-targets/commands/delete-other-k8s-resources.sh @@ -0,0 +1,23 @@ +#!/bin/bash +# +# SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors +# +# SPDX-License-Identifier: Apache-2.0 + +set -o errexit + +COMPONENT_DIR="$(dirname $0)/.." +cd "${COMPONENT_DIR}" +COMPONENT_DIR="$(pwd)" +echo "COMPONENT_DIR: ${COMPONENT_DIR}" + +source "${COMPONENT_DIR}/commands/settings" + +echo "deleting target" +kubectl delete target "self-target" -n "${NAMESPACE}" --kubeconfig="${RESOURCE_CLUSTER_KUBECONFIG_PATH}" + +echo "deleting clusterrolebinding" +kubectl delete clusterrolebinding "landscaper:guided-tour:self" --kubeconfig="${RESOURCE_CLUSTER_KUBECONFIG_PATH}" + +echo "deleting serviceaccount" +kubectl delete serviceaccount "self-serviceaccount" -n "${NAMESPACE}" --kubeconfig="${RESOURCE_CLUSTER_KUBECONFIG_PATH}" diff --git a/docs/guided-tour/targets/02-self-targets/commands/deploy-k8s-resources.sh b/docs/guided-tour/targets/02-self-targets/commands/deploy-k8s-resources.sh new file mode 100755 index 000000000..d3181d5f3 --- /dev/null +++ b/docs/guided-tour/targets/02-self-targets/commands/deploy-k8s-resources.sh @@ -0,0 +1,45 @@ +#!/bin/bash +# +# SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors +# +# SPDX-License-Identifier: Apache-2.0 + +set -o errexit + +COMPONENT_DIR="$(dirname $0)/.." +cd "${COMPONENT_DIR}" +COMPONENT_DIR="$(pwd)" +echo "COMPONENT_DIR: ${COMPONENT_DIR}" + +source "${COMPONENT_DIR}/commands/settings" + +TMP_DIR=`mktemp -d` +echo "TMP_DIR: ${TMP_DIR}" + +echo "creating serviceaccount" +outputFile="${TMP_DIR}/serviceaccount.yaml" +export namespace="${NAMESPACE}" +inputFile="${COMPONENT_DIR}/installation/serviceaccount.yaml.tpl" +envsubst < ${inputFile} > ${outputFile} +kubectl apply -f ${outputFile} --kubeconfig="${RESOURCE_CLUSTER_KUBECONFIG_PATH}" + +echo "creating clusterrolebinding" +outputFile="${TMP_DIR}/clusterrolebinding.yaml" +export namespace="${NAMESPACE}" +inputFile="${COMPONENT_DIR}/installation/clusterrolebinding.yaml.tpl" +envsubst < ${inputFile} > ${outputFile} +kubectl apply -f ${outputFile} --kubeconfig="${RESOURCE_CLUSTER_KUBECONFIG_PATH}" + +echo "creating target" +outputFile="${TMP_DIR}/target.yaml" +export namespace="${NAMESPACE}" +inputFile="${COMPONENT_DIR}/installation/target.yaml.tpl" +envsubst < ${inputFile} > ${outputFile} +kubectl apply -f ${outputFile} --kubeconfig="${RESOURCE_CLUSTER_KUBECONFIG_PATH}" + +echo "creating installation" +outputFile="${TMP_DIR}/installation.yaml" +export namespace="${NAMESPACE}" +inputFile="${COMPONENT_DIR}/installation/installation.yaml.tpl" +envsubst < ${inputFile} > ${outputFile} +kubectl apply -f ${outputFile} --kubeconfig="${RESOURCE_CLUSTER_KUBECONFIG_PATH}" diff --git a/docs/guided-tour/targets/02-self-targets/commands/settings b/docs/guided-tour/targets/02-self-targets/commands/settings new file mode 100644 index 000000000..8f1cbe64d --- /dev/null +++ b/docs/guided-tour/targets/02-self-targets/commands/settings @@ -0,0 +1,5 @@ +# path to the kubeconfig of the resource cluster, i.e. the cluster on which installations, targets, etc. are created +RESOURCE_CLUSTER_KUBECONFIG_PATH="/Users/${USER}/tmp/kubes/kubeconfig.yaml" + +# namespace for resources in the resource cluster +NAMESPACE="cu-example" diff --git a/docs/guided-tour/targets/02-self-targets/images/self-targets.png b/docs/guided-tour/targets/02-self-targets/images/self-targets.png new file mode 100644 index 0000000000000000000000000000000000000000..f81f0f2336d18ac4b0a446ccbd8721818f43c718 GIT binary patch literal 96452 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

H!u@!W}QL)ag7;mkB4~#gQ^EOW03E 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 literal 0 HcmV?d00001 diff --git a/docs/guided-tour/targets/02-self-targets/installation/clusterrolebinding.yaml.tpl b/docs/guided-tour/targets/02-self-targets/installation/clusterrolebinding.yaml.tpl new file mode 100644 index 000000000..36dd7f0c5 --- /dev/null +++ b/docs/guided-tour/targets/02-self-targets/installation/clusterrolebinding.yaml.tpl @@ -0,0 +1,12 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: landscaper:guided-tour:self +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin +subjects: + - kind: ServiceAccount + name: self-serviceaccount + namespace: ${namespace} diff --git a/docs/guided-tour/targets/02-self-targets/installation/installation.yaml.tpl b/docs/guided-tour/targets/02-self-targets/installation/installation.yaml.tpl new file mode 100644 index 000000000..f91b10e49 --- /dev/null +++ b/docs/guided-tour/targets/02-self-targets/installation/installation.yaml.tpl @@ -0,0 +1,53 @@ +apiVersion: landscaper.gardener.cloud/v1alpha1 +kind: Installation +metadata: + name: self-inst + namespace: ${namespace} + annotations: + landscaper.gardener.cloud/operation: reconcile + +spec: + + imports: + targets: + - name: cluster + target: self-target + + blueprint: + inline: + filesystem: + blueprint.yaml: | + apiVersion: landscaper.gardener.cloud/v1alpha1 + kind: Blueprint + jsonSchema: "https://json-schema.org/draft/2019-09/schema" + + imports: + - name: cluster + type: target + targetType: landscaper.gardener.cloud/kubernetes-cluster + + deployExecutions: + - name: default + type: GoTemplate + template: | + deployItems: + - name: default-deploy-item + type: landscaper.gardener.cloud/kubernetes-manifest + + target: + import: cluster + + config: + apiVersion: manifest.deployer.landscaper.gardener.cloud/v1alpha2 + kind: ProviderConfiguration + updateStrategy: update + manifests: + - policy: manage + manifest: + apiVersion: v1 + kind: ConfigMap + metadata: + name: self-target-example + namespace: example + data: + testData: hello diff --git a/docs/guided-tour/targets/02-self-targets/installation/serviceaccount.yaml.tpl b/docs/guided-tour/targets/02-self-targets/installation/serviceaccount.yaml.tpl new file mode 100644 index 000000000..9b131ad5d --- /dev/null +++ b/docs/guided-tour/targets/02-self-targets/installation/serviceaccount.yaml.tpl @@ -0,0 +1,5 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: self-serviceaccount + namespace: ${namespace} diff --git a/docs/guided-tour/targets/02-self-targets/installation/target.yaml.tpl b/docs/guided-tour/targets/02-self-targets/installation/target.yaml.tpl new file mode 100644 index 000000000..f1815c5c0 --- /dev/null +++ b/docs/guided-tour/targets/02-self-targets/installation/target.yaml.tpl @@ -0,0 +1,12 @@ +apiVersion: landscaper.gardener.cloud/v1alpha1 +kind: Target +metadata: + name: self-target + namespace: ${namespace} +spec: + type: landscaper.gardener.cloud/kubernetes-cluster + config: + selfConfig: + serviceAccount: + name: self-serviceaccount + expirationSeconds: 3600 diff --git a/docs/usage/Targets.md b/docs/usage/Targets.md index 4d814c6e0..65e8bc7c9 100644 --- a/docs/usage/Targets.md +++ b/docs/usage/Targets.md @@ -277,4 +277,66 @@ This target can be used in Landscaper Installations in the same way as other Tar The following picture gives an overview about the cluster settings and k8s resources required to set up a trust relationship between the resource and the target cluster. -![OIDC Targets](images/oidc-targets.png) \ No newline at end of file +![OIDC Targets](images/oidc-targets.png) + + +## Targets to the Landscaper Resource Cluster ("Self Targets") + +You can use the Landscaper to deploy resources to the Landscaper resource cluster itself, i.e. the cluster on which the +Installations and Targets reside. You can achieve this with a special sort of Target, a so-called "Self Target". + +First, create a ServiceAccount on the resource cluster in some namespace. Note that this ServiceAccount must belong to +the same namespace as the Self Target that we are going to create, and any Installation that uses the Target. + +```yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: + namespace: +``` + +The ServiceAccount must have enough permissions on the resource cluster to perform the deployment, which your +Installation defines. There are the following alternatives to grant the ServiceAccount permissions: + +- You can create a (Cluster)RoleBinding, that binds the ServiceAccount to an appropriate (Cluster)Role. + +- If you are using a Landscaper instance managed by the + [Landscaper-as-a-Service](https://github.com/gardener/landscaper-service), + just add the ServiceAccount to the SubjectList `subjects` in namespace `ls-user` on the resource cluster. + It will then automatically get the same roles as a user of the Landscaper instance. + + ```yaml + apiVersion: landscaper-service.gardener.cloud/v1alpha1 + kind: SubjectList + metadata: + name: subjects + namespace: ls-user + ... + spec: + subjects: + - kind: ServiceAccount + name: + namespace: + - ... + ``` + +Next, create the Self Target. It is a special sort of Target with the following structure: + + ```yaml + apiVersion: landscaper.gardener.cloud/v1alpha1 + kind: Target + metadata: + name: + namespace: + spec: + config: + selfConfig: + serviceAccount: + name: + expirationSeconds: # optional, defaults to 86400 = 60 * 60 * 24 + type: landscaper.gardener.cloud/kubernetes-cluster + ``` + +Now you can use this Target as usual in Installations. +There is an [example in the Guided-Tour](../guided-tour/targets/02-self-targets). diff --git a/pkg/deployer/helm/add.go b/pkg/deployer/helm/add.go index 9e0524932..32a96e1ac 100644 --- a/pkg/deployer/helm/add.go +++ b/pkg/deployer/helm/add.go @@ -9,7 +9,6 @@ import ( "fmt" "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/controller" "sigs.k8s.io/controller-runtime/pkg/manager" @@ -40,10 +39,7 @@ func AddDeployerToManager(lsUncachedClient, lsCachedClient, hostUncachedClient, } log.Info("access to critical problems allowed") - d, err := NewDeployer(lsUncachedClient, lsCachedClient, hostUncachedClient, hostCachedClient, - log, - config, - ) + d, err := NewDeployer(lsUncachedClient, lsCachedClient, hostUncachedClient, hostCachedClient, lsMgr.GetConfig(), log, config) if err != nil { return err } diff --git a/pkg/deployer/helm/deletionmanager_test.go b/pkg/deployer/helm/deletionmanager_test.go index 036d82fb2..7fb48e11c 100644 --- a/pkg/deployer/helm/deletionmanager_test.go +++ b/pkg/deployer/helm/deletionmanager_test.go @@ -6,16 +6,13 @@ import ( "encoding/json" "time" - "k8s.io/utils/ptr" - - "github.com/gardener/landscaper/pkg/utils" - "github.com/google/uuid" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/onsi/gomega/types" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/client-go/tools/record" + "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/reconcile" @@ -27,6 +24,7 @@ import ( "github.com/gardener/landscaper/pkg/deployer/helm" deployerlib "github.com/gardener/landscaper/pkg/deployer/lib" "github.com/gardener/landscaper/pkg/deployer/lib/timeout" + "github.com/gardener/landscaper/pkg/utils" testutils "github.com/gardener/landscaper/test/utils" "github.com/gardener/landscaper/test/utils/envtest" "github.com/gardener/landscaper/test/utils/matchers" @@ -67,10 +65,10 @@ var _ = Describe("Deletion Manager", func() { Expect(err).ToNot(HaveOccurred()) resources = &resourceBuilder{state.Namespace} - deployer, err := helm.NewDeployer(testenv.Client, testenv.Client, testenv.Client, testenv.Client, logging.Discard(), helmv1alpha1.Configuration{}) + deployer, err := helm.NewDeployer(testenv.Client, testenv.Client, testenv.Client, testenv.Client, nil, logging.Discard(), helmv1alpha1.Configuration{}) Expect(err).ToNot(HaveOccurred()) - ctrl = deployerlib.NewController( + ctrl = deployerlib.NewController(nil, testenv.Client, testenv.Client, testenv.Client, testenv.Client, utils.NewFinishedObjectCache(), api.LandscaperScheme, diff --git a/pkg/deployer/helm/deployer.go b/pkg/deployer/helm/deployer.go index f9399a8dc..2bf52a78d 100644 --- a/pkg/deployer/helm/deployer.go +++ b/pkg/deployer/helm/deployer.go @@ -8,6 +8,7 @@ import ( "context" "time" + "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/client" lsv1alpha1 "github.com/gardener/landscaper/apis/core/v1alpha1" @@ -36,15 +37,16 @@ const ( ) // NewDeployer creates a new deployer that reconciles deploy items of type helm. -func NewDeployer(lsUncachedClient, lsCachedClient, hostUncachedClient, hostCachedClient client.Client, - log logging.Logger, - config helmv1alpha1.Configuration) (deployerlib.Deployer, error) { +func NewDeployer( + lsUncachedClient, lsCachedClient, hostUncachedClient, hostCachedClient client.Client, lsRestConfig *rest.Config, + log logging.Logger, config helmv1alpha1.Configuration) (deployerlib.Deployer, error) { dep := &deployer{ lsUncachedClient: lsUncachedClient, lsCachedClient: lsCachedClient, hostUncachedClient: hostUncachedClient, hostCachedClient: hostCachedClient, + lsRestConfig: lsRestConfig, log: log, config: config, hooks: extension.ReconcileExtensionHooks{}, @@ -58,6 +60,7 @@ type deployer struct { lsCachedClient client.Client hostUncachedClient client.Client hostCachedClient client.Client + lsRestConfig *rest.Config log logging.Logger config helmv1alpha1.Configuration @@ -69,7 +72,7 @@ func (d *deployer) Reconcile(ctx context.Context, lsCtx *lsv1alpha1.Context, di return err } - helm, err := New(d.lsUncachedClient, d.lsCachedClient, d.hostUncachedClient, d.hostCachedClient, d.config, di, rt, lsCtx) + helm, err := New(d.lsUncachedClient, d.lsCachedClient, d.hostUncachedClient, d.hostCachedClient, d.lsRestConfig, d.config, di, rt, lsCtx) if err != nil { err = lserrors.NewWrappedError(err, "Reconcile", "newRootLogger", err.Error()) return err @@ -106,7 +109,7 @@ func (d *deployer) Delete(ctx context.Context, lsCtx *lsv1alpha1.Context, di *ls return err } - helm, err := New(d.lsUncachedClient, d.lsCachedClient, d.hostUncachedClient, d.hostCachedClient, d.config, di, rt, lsCtx) + helm, err := New(d.lsUncachedClient, d.lsCachedClient, d.hostUncachedClient, d.hostCachedClient, d.lsRestConfig, d.config, di, rt, lsCtx) if err != nil { return err } @@ -129,7 +132,7 @@ func (d *deployer) ExtensionHooks() extension.ReconcileExtensionHooks { func (d *deployer) NextReconcile(ctx context.Context, last time.Time, di *lsv1alpha1.DeployItem) (*time.Time, error) { // todo: directly parse deploy items - helm, err := New(d.lsUncachedClient, d.lsCachedClient, d.hostUncachedClient, d.hostCachedClient, d.config, di, nil, nil) + helm, err := New(d.lsUncachedClient, d.lsCachedClient, d.hostUncachedClient, d.hostCachedClient, d.lsRestConfig, d.config, di, nil, nil) if err != nil { return nil, err } diff --git a/pkg/deployer/helm/ensure.go b/pkg/deployer/helm/ensure.go index 836c9a12e..02c5813e4 100644 --- a/pkg/deployer/helm/ensure.go +++ b/pkg/deployer/helm/ensure.go @@ -12,7 +12,6 @@ import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/serializer" - "k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes/scheme" "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" @@ -44,9 +43,8 @@ func (h *Helm) ApplyFiles(ctx context.Context, filesForManifestDeployer, crdsFor currOp := "ApplyFile" logger, ctx := logging.FromContextOrNew(ctx, []interface{}{lc.KeyMethod, currOp}) - _, targetClient, targetClientSet, err := h.TargetClient(ctx) - if err != nil { - return lserrors.NewWrappedError(err, currOp, "TargetClusterClient", err.Error()) + if err := h.ensureTargetAccess(ctx); err != nil { + return lserrors.NewWrappedError(err, currOp, "ensureTargetAccess", err.Error()) } if h.ProviderStatus == nil { @@ -66,7 +64,7 @@ func (h *Helm) ApplyFiles(ctx context.Context, filesForManifestDeployer, crdsFor if shouldUseRealHelmDeployer { // Apply helm install/upgrade. Afterwards get the list of deployed resources by helm get release. // The list is filtered, i.e. it contains only the resources that are needed for the default readiness check. - realHelmDeployer := realhelmdeployer.NewRealHelmDeployer(ch, h.ProviderConfiguration, h.TargetRestConfig, targetClientSet, h.DeployItem) + realHelmDeployer := realhelmdeployer.NewRealHelmDeployer(ch, h.ProviderConfiguration, h.targetAccess, h.DeployItem) deployErr = realHelmDeployer.Deploy(ctx) if deployErr == nil { managedResourceStatusList, err := realHelmDeployer.GetManagedResourcesStatus(ctx) @@ -82,7 +80,7 @@ func (h *Helm) ApplyFiles(ctx context.Context, filesForManifestDeployer, crdsFor return err } - deployErr = h.applyManifests(ctx, targetClient, targetClientSet, manifests) + deployErr = h.applyManifests(ctx, manifests) } // common error handling for deploy errors (h.applyManifests / realHelmDeployer.Deploy) @@ -95,6 +93,7 @@ func (h *Helm) ApplyFiles(ctx context.Context, filesForManifestDeployer, crdsFor return deployErr } + var err error h.DeployItem.Status.ProviderStatus, err = kutil.ConvertToRawExtension(h.ProviderStatus, HelmScheme) if err != nil { return lserrors.NewWrappedError(err, currOp, "ProviderStatus", err.Error()) @@ -108,7 +107,7 @@ func (h *Helm) ApplyFiles(ctx context.Context, filesForManifestDeployer, crdsFor return err } - if err := h.checkResourcesReady(ctx, targetClient, !shouldUseRealHelmDeployer); err != nil { + if err := h.checkResourcesReady(ctx, h.targetAccess.TargetClient(), !shouldUseRealHelmDeployer); err != nil { return err } @@ -116,7 +115,7 @@ func (h *Helm) ApplyFiles(ctx context.Context, filesForManifestDeployer, crdsFor return err } - if err := h.readExportValues(ctx, currOp, targetClient, exports); err != nil { + if err := h.readExportValues(ctx, currOp, h.targetAccess.TargetClient(), exports); err != nil { return err } @@ -125,8 +124,7 @@ func (h *Helm) ApplyFiles(ctx context.Context, filesForManifestDeployer, crdsFor return nil } -func (h *Helm) applyManifests(ctx context.Context, targetClient client.Client, targetClientSet kubernetes.Interface, - manifests []managedresource.Manifest) error { +func (h *Helm) applyManifests(ctx context.Context, manifests []managedresource.Manifest) error { if _, err := timeout.TimeoutExceeded(ctx, h.DeployItem, TimeoutCheckpointHelmStartApplyManifests); err != nil { return err @@ -134,8 +132,8 @@ func (h *Helm) applyManifests(ctx context.Context, targetClient client.Client, t applier := resourcemanager.NewManifestApplier(resourcemanager.ManifestApplierOptions{ Decoder: serializer.NewCodecFactory(scheme.Scheme).UniversalDecoder(), - KubeClient: targetClient, - Clientset: targetClientSet, + KubeClient: h.targetAccess.TargetClient(), + Clientset: h.targetAccess.TargetClientSet(), DefaultNamespace: h.ProviderConfiguration.Namespace, DeployItemName: h.DeployItem.Name, DeployItem: h.DeployItem, @@ -148,6 +146,7 @@ func (h *Helm) applyManifests(ctx context.Context, targetClient client.Client, t DeletionGroupsDuringUpdate: h.ProviderConfiguration.DeletionGroupsDuringUpdate, InterruptionChecker: interruption.NewStandardInterruptionChecker(h.DeployItem, h.lsUncachedClient), LsUncachedClient: h.lsUncachedClient, + LsRestConfig: h.lsRestConfig, }) _, err := applier.Apply(ctx) @@ -254,6 +253,7 @@ func (h *Helm) checkResourcesReady(ctx context.Context, client client.Client, fa InterruptionChecker: interruption.NewStandardInterruptionChecker(h.DeployItem, h.lsUncachedClient), LsClient: h.lsUncachedClient, DeployItem: h.DeployItem, + LsRestConfig: h.lsRestConfig, } err := customReadinessCheck.CheckResourcesReady(ctx) if err != nil { @@ -279,6 +279,7 @@ func (h *Helm) readExportValues(ctx context.Context, currOp string, targetClient InterruptionChecker: interruption.NewStandardInterruptionChecker(h.DeployItem, h.lsUncachedClient), LsClient: h.lsUncachedClient, DeployItem: h.DeployItem, + LsRestConfig: h.lsRestConfig, } resourceExports, err := resourcemanager.NewExporter(opts).Export(ctx, exportDefinition) @@ -316,21 +317,21 @@ func (h *Helm) deleteManifestsInGroups(ctx context.Context) error { return h.Writer().UpdateDeployItem(ctx, read_write_layer.W000067, h.DeployItem) } - _, targetClient, _, err := h.TargetClient(ctx) - if err != nil { + if err := h.ensureTargetAccess(ctx); err != nil { return err } interruptionChecker := interruption.NewStandardInterruptionChecker(h.DeployItem, h.lsUncachedClient) - err = resourcemanager.DeleteManagedResources( + err := resourcemanager.DeleteManagedResources( ctx, h.lsUncachedClient, h.ProviderStatus.ManagedResources, h.ProviderConfiguration.DeletionGroups, - targetClient, + h.targetAccess.TargetClient(), h.DeployItem, - interruptionChecker) + interruptionChecker, + h.lsRestConfig) if err != nil { return fmt.Errorf("failed deleting managed resources: %w", err) } @@ -351,15 +352,13 @@ func (h *Helm) deleteManifestsWithRealHelmDeployer(ctx context.Context, di *lsv1 return h.Writer().UpdateDeployItem(ctx, read_write_layer.W000047, h.DeployItem) } - _, _, targetClientSet, err := h.TargetClient(ctx) - if err != nil { + if err := h.ensureTargetAccess(ctx); err != nil { return err } - realHelmDeployer := realhelmdeployer.NewRealHelmDeployer(nil, h.ProviderConfiguration, - h.TargetRestConfig, targetClientSet, di) + realHelmDeployer := realhelmdeployer.NewRealHelmDeployer(nil, h.ProviderConfiguration, h.targetAccess, di) - err = realHelmDeployer.Undeploy(ctx) + err := realHelmDeployer.Undeploy(ctx) if err != nil { return err } diff --git a/pkg/deployer/helm/helm.go b/pkg/deployer/helm/helm.go index 78502cefc..c0cf049a8 100644 --- a/pkg/deployer/helm/helm.go +++ b/pkg/deployer/helm/helm.go @@ -6,7 +6,6 @@ package helm import ( "context" - "errors" "strings" "helm.sh/helm/v3/pkg/chart" @@ -14,7 +13,6 @@ import ( "helm.sh/helm/v3/pkg/engine" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/yaml" - "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" @@ -54,6 +52,7 @@ type Helm struct { lsCachedClient client.Client hostUncachedClient client.Client hostCachedClient client.Client + lsRestConfig *rest.Config Configuration helmv1alpha1.Configuration @@ -63,13 +62,13 @@ type Helm struct { ProviderConfiguration *helmv1alpha1.ProviderConfiguration ProviderStatus *helmv1alpha1.ProviderStatus - TargetKubeClient client.Client - TargetRestConfig *rest.Config - TargetClientSet kubernetes.Interface + targetAccess *lib.TargetAccess } // New creates a new internal helm item -func New(lsUncachedClient, lsCachedClient, hostUncachedClient, hostCachedClient client.Client, +func New( + lsUncachedClient, lsCachedClient, hostUncachedClient, hostCachedClient client.Client, + lsRestConfig *rest.Config, helmconfig helmv1alpha1.Configuration, item *lsv1alpha1.DeployItem, rt *lsv1alpha1.ResolvedTarget, @@ -99,6 +98,7 @@ func New(lsUncachedClient, lsCachedClient, hostUncachedClient, hostCachedClient } return &Helm{ + lsRestConfig: lsRestConfig, lsUncachedClient: lsUncachedClient, lsCachedClient: lsCachedClient, hostUncachedClient: hostUncachedClient, @@ -112,15 +112,21 @@ func New(lsUncachedClient, lsCachedClient, hostUncachedClient, hostCachedClient }, nil } +func (h *Helm) ensureTargetAccess(ctx context.Context) (err error) { + if h.targetAccess == nil { + h.targetAccess, err = lib.NewTargetAccess(ctx, h.Target, h.lsUncachedClient, h.lsRestConfig) + } + return err +} + // Template loads the specified helm chart // and templates it with the given values. func (h *Helm) Template(ctx context.Context) (map[string]string, map[string]string, map[string]interface{}, *chart.Chart, lserrors.LsError) { currOp := "TemplateChart" - restConfig, _, _, err := h.TargetClient(ctx) - if err != nil { - return nil, nil, nil, nil, lserrors.NewWrappedError(err, currOp, "GetTargetClient", err.Error()) + if err := h.ensureTargetAccess(ctx); err != nil { + return nil, nil, nil, nil, lserrors.NewWrappedError(err, currOp, "ensureTargetAccess", err.Error()) } // download chart @@ -168,7 +174,7 @@ func (h *Helm) Template(ctx context.Context) (map[string]string, map[string]stri crdsForManifestDeployer := map[string]string{} shouldUseRealHelmDeployer := ptr.Deref[bool](h.ProviderConfiguration.HelmDeployment, true) if !shouldUseRealHelmDeployer { - filesForManifestDeployer, err = engine.RenderWithClient(ch, values, restConfig) + filesForManifestDeployer, err = engine.RenderWithClient(ch, values, h.targetAccess.TargetRestConfig()) if err != nil { return nil, nil, nil, nil, lserrors.NewWrappedError( err, currOp, "RenderHelmValues", err.Error(), lsv1alpha1.ErrorConfigurationProblem) @@ -182,25 +188,6 @@ func (h *Helm) Template(ctx context.Context) (map[string]string, map[string]stri return filesForManifestDeployer, crdsForManifestDeployer, values, ch, nil } -func (h *Helm) TargetClient(ctx context.Context) (*rest.Config, client.Client, kubernetes.Interface, error) { - if h.TargetKubeClient != nil { - return h.TargetRestConfig, h.TargetKubeClient, h.TargetClientSet, nil - } - if h.Target != nil { - restConfig, kubeClient, clientset, err := lib.GetRestConfigAndClientAndClientSet(ctx, h.Target, h.lsUncachedClient) - if err != nil { - return nil, nil, nil, err - } - - h.TargetRestConfig = restConfig - h.TargetKubeClient = kubeClient - h.TargetClientSet = clientset - - return restConfig, kubeClient, clientset, nil - } - return nil, nil, nil, errors.New("neither a target nor kubeconfig are defined") -} - func (h *Helm) isDownloadInfoError(err error) bool { msg := err.Error() return strings.Contains(msg, "no chart name found") || diff --git a/pkg/deployer/helm/helm_suite_test.go b/pkg/deployer/helm/helm_suite_test.go index c945bd55d..78124b711 100644 --- a/pkg/deployer/helm/helm_suite_test.go +++ b/pkg/deployer/helm/helm_suite_test.go @@ -114,7 +114,7 @@ var _ = Describe("Template", func() { lsCtx := &lsv1alpha1.Context{} lsCtx.Name = lsv1alpha1.DefaultContextName lsCtx.Namespace = item.Namespace - h, err := helm.New(testenv.Client, testenv.Client, testenv.Client, testenv.Client, helmv1alpha1.Configuration{}, item, rt, lsCtx) + h, err := helm.New(testenv.Client, testenv.Client, testenv.Client, testenv.Client, nil, helmv1alpha1.Configuration{}, item, rt, lsCtx) Expect(err).ToNot(HaveOccurred()) files, crds, _, _, err := h.Template(ctx) Expect(err).ToNot(HaveOccurred()) diff --git a/pkg/deployer/helm/realhelmdeployer/real_helm_deployer.go b/pkg/deployer/helm/realhelmdeployer/real_helm_deployer.go index 712dc77a4..4902b88a4 100644 --- a/pkg/deployer/helm/realhelmdeployer/real_helm_deployer.go +++ b/pkg/deployer/helm/realhelmdeployer/real_helm_deployer.go @@ -13,6 +13,8 @@ import ( "strings" "sync" + "github.com/gardener/landscaper/pkg/deployer/lib" + "helm.sh/helm/v3/pkg/action" "helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/kube" @@ -64,8 +66,8 @@ type RealHelmDeployer struct { mutex sync.RWMutex } -func NewRealHelmDeployer(ch *chart.Chart, providerConfig *helmv1alpha1.ProviderConfiguration, targetRestConfig *rest.Config, - clientset kubernetes.Interface, di *lsv1alpha1.DeployItem) *RealHelmDeployer { +func NewRealHelmDeployer(ch *chart.Chart, providerConfig *helmv1alpha1.ProviderConfiguration, + targetAccess *lib.TargetAccess, di *lsv1alpha1.DeployItem) *RealHelmDeployer { return &RealHelmDeployer{ chart: ch, @@ -75,8 +77,8 @@ func NewRealHelmDeployer(ch *chart.Chart, providerConfig *helmv1alpha1.ProviderC rawValues: providerConfig.Values, helmConfig: providerConfig.HelmDeploymentConfig, createNamespace: providerConfig.CreateNamespace, - targetRestConfig: targetRestConfig, - apiResourceHandler: resourcemanager.CreateApiResourceHandler(clientset), + targetRestConfig: targetAccess.TargetRestConfig(), + apiResourceHandler: resourcemanager.CreateApiResourceHandler(targetAccess.TargetClientSet()), helmSecretManager: nil, di: di, messages: make([]string, 0), diff --git a/pkg/deployer/helm/test/e2e_test.go b/pkg/deployer/helm/test/e2e_test.go index 979383c4e..874a52d99 100644 --- a/pkg/deployer/helm/test/e2e_test.go +++ b/pkg/deployer/helm/test/e2e_test.go @@ -53,13 +53,11 @@ var _ = Describe("Helm Deployer", func() { ctx := context.Background() defer ctx.Done() - deployer, err := helmctrl.NewDeployer(testenv.Client, testenv.Client, testenv.Client, testenv.Client, - logging.Discard(), - helmv1alpha1.Configuration{}, - ) + deployer, err := helmctrl.NewDeployer(testenv.Client, testenv.Client, testenv.Client, testenv.Client, nil, + logging.Discard(), helmv1alpha1.Configuration{}) Expect(err).ToNot(HaveOccurred()) - ctrl := deployerlib.NewController( + ctrl := deployerlib.NewController(nil, testenv.Client, testenv.Client, testenv.Client, testenv.Client, utils.NewFinishedObjectCache(), api.LandscaperScheme, diff --git a/pkg/deployer/lib/controller.go b/pkg/deployer/lib/controller.go index 2f691bae1..9b5371766 100644 --- a/pkg/deployer/lib/controller.go +++ b/pkg/deployer/lib/controller.go @@ -18,6 +18,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/errors" + "k8s.io/client-go/rest" "k8s.io/client-go/tools/record" "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" @@ -98,7 +99,8 @@ func Add(lsUncachedClient, lsCachedClient, hostUncachedClient, hostCachedClient if err := args.Validate(); err != nil { return err } - con := NewController(lsUncachedClient, lsCachedClient, hostUncachedClient, hostCachedClient, + con := NewController(lsMgr.GetConfig(), + lsUncachedClient, lsCachedClient, hostUncachedClient, hostCachedClient, finishedObjectCache, lsMgr.GetScheme(), lsMgr.GetEventRecorderFor(args.Name), @@ -119,6 +121,7 @@ func Add(lsUncachedClient, lsCachedClient, hostUncachedClient, hostCachedClient // controller reconciles deployitems and delegates the business logic to the configured Deployer. type controller struct { + lsRestConfig *rest.Config lsUncachedClient client.Client lsCachedClient client.Client hostUncachedClient client.Client @@ -143,7 +146,8 @@ type controller struct { } // NewController creates a new generic deployitem controller. -func NewController(lsUncachedClient, lsCachedClient, hostUncachedClient, hostCachedClient client.Client, +func NewController(lsRestConfig *rest.Config, + lsUncachedClient, lsCachedClient, hostUncachedClient, hostCachedClient client.Client, finishedObjectCache *lsutil.FinishedObjectCache, lsScheme *runtime.Scheme, lsEventRecorder record.EventRecorder, @@ -156,6 +160,7 @@ func NewController(lsUncachedClient, lsCachedClient, hostUncachedClient, hostCac wc := lsutil.NewWorkerCounter(maxNumberOfWorkers) return &controller{ + lsRestConfig: lsRestConfig, lsUncachedClient: lsUncachedClient, lsCachedClient: lsCachedClient, hostUncachedClient: hostUncachedClient, diff --git a/pkg/deployer/lib/readinesscheck/customreadinesscheck.go b/pkg/deployer/lib/readinesscheck/customreadinesscheck.go index dbee280bf..4400afdf6 100644 --- a/pkg/deployer/lib/readinesscheck/customreadinesscheck.go +++ b/pkg/deployer/lib/readinesscheck/customreadinesscheck.go @@ -11,6 +11,8 @@ import ( "reflect" "strings" + "k8s.io/client-go/rest" + "github.com/pkg/errors" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -39,6 +41,7 @@ type CustomReadinessCheck struct { InterruptionChecker interruption.InterruptionChecker LsClient client.Client DeployItem *lsv1alpha1.DeployItem + LsRestConfig *rest.Config } // CheckResourcesReady starts a custom readiness check by checking the readiness of the submitted resources @@ -48,7 +51,7 @@ func (c *CustomReadinessCheck) CheckResourcesReady(ctx context.Context) error { return nil } - targetClient, err := lib.GetTargetClient(ctx, c.Client, c.LsClient, c.DeployItem, c.Configuration.TargetName) + targetClient, err := lib.GetTargetClientConsideringSecondaryTarget(ctx, c.Client, c.LsClient, c.DeployItem, c.Configuration.TargetName, c.LsRestConfig) if err != nil { return err } diff --git a/pkg/deployer/lib/resourcemanager/deletiongroup.go b/pkg/deployer/lib/resourcemanager/deletiongroup.go index 343cde2cc..70c15ee2e 100644 --- a/pkg/deployer/lib/resourcemanager/deletiongroup.go +++ b/pkg/deployer/lib/resourcemanager/deletiongroup.go @@ -12,15 +12,15 @@ import ( apimeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/client" - "github.com/gardener/landscaper/pkg/deployer/lib" - lsv1alpha1 "github.com/gardener/landscaper/apis/core/v1alpha1" "github.com/gardener/landscaper/apis/deployer/utils/managedresource" kutil "github.com/gardener/landscaper/controller-utils/pkg/kubernetes" "github.com/gardener/landscaper/controller-utils/pkg/logging" lc "github.com/gardener/landscaper/controller-utils/pkg/logging/constants" + "github.com/gardener/landscaper/pkg/deployer/lib" "github.com/gardener/landscaper/pkg/deployer/lib/interruption" "github.com/gardener/landscaper/pkg/deployer/lib/timeout" ) @@ -43,6 +43,7 @@ func NewDeletionGroup( deployItem *lsv1alpha1.DeployItem, primaryTargetClient client.Client, interruptionChecker interruption.InterruptionChecker, + lsRestConfig *rest.Config, ) (group *DeletionGroup, err error) { if definition.IsPredefined() && definition.IsCustom() { return nil, fmt.Errorf("invalid deletion group: predefinedResourceGroup and customResourceGroup must not both be set") @@ -60,7 +61,7 @@ func NewDeletionGroup( targetClient := primaryTargetClient if isCustomWithSecondaryTarget { - targetClient, err = lib.GetTargetClient(ctx, primaryTargetClient, lsUncachedClient, deployItem, definition.CustomResourceGroup.TargetName) + targetClient, err = lib.GetTargetClientConsideringSecondaryTarget(ctx, primaryTargetClient, lsUncachedClient, deployItem, definition.CustomResourceGroup.TargetName, lsRestConfig) if err != nil { return nil, err } diff --git a/pkg/deployer/lib/resourcemanager/deletionmanager.go b/pkg/deployer/lib/resourcemanager/deletionmanager.go index bf12c71f5..8918b76d2 100644 --- a/pkg/deployer/lib/resourcemanager/deletionmanager.go +++ b/pkg/deployer/lib/resourcemanager/deletionmanager.go @@ -3,6 +3,7 @@ package resourcemanager import ( "context" + "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/client" lsv1alpha1 "github.com/gardener/landscaper/apis/core/v1alpha1" @@ -18,6 +19,7 @@ func DeleteManagedResources( targetClient client.Client, deployItem *lsv1alpha1.DeployItem, interruptionChecker interruption.InterruptionChecker, + lsRestConfig *rest.Config, ) error { if len(managedResources) == 0 { return nil @@ -32,7 +34,7 @@ func DeleteManagedResources( groups := make([]*DeletionGroup, len(groupDefinitions)) for i := range groupDefinitions { var err error - groups[i], err = NewDeletionGroup(ctx, lsUncachedClient, groupDefinitions[i], deployItem, targetClient, interruptionChecker) + groups[i], err = NewDeletionGroup(ctx, lsUncachedClient, groupDefinitions[i], deployItem, targetClient, interruptionChecker, lsRestConfig) if err != nil { return err } diff --git a/pkg/deployer/lib/resourcemanager/exporter.go b/pkg/deployer/lib/resourcemanager/exporter.go index bce62af05..028b6d6d9 100644 --- a/pkg/deployer/lib/resourcemanager/exporter.go +++ b/pkg/deployer/lib/resourcemanager/exporter.go @@ -10,6 +10,7 @@ import ( "time" "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/client" lsv1alpha1 "github.com/gardener/landscaper/apis/core/v1alpha1" @@ -32,6 +33,7 @@ type ExporterOptions struct { InterruptionChecker interruption.InterruptionChecker LsClient client.Client DeployItem *lsv1alpha1.DeployItem + LsRestConfig *rest.Config } // Exporter defines the export of data from manifests. @@ -40,6 +42,7 @@ type Exporter struct { interruptionChecker interruption.InterruptionChecker lsClient client.Client deployItem *lsv1alpha1.DeployItem + lsRestConfig *rest.Config } // NewExporter creates a new exporter. @@ -49,6 +52,7 @@ func NewExporter(opts ExporterOptions) *Exporter { interruptionChecker: opts.InterruptionChecker, lsClient: opts.LsClient, deployItem: opts.DeployItem, + lsRestConfig: opts.LsRestConfig, } if exporter.interruptionChecker == nil { @@ -107,7 +111,7 @@ func (e *Exporter) Export(ctx context.Context, exports *managedresource.Exports) } func (e *Exporter) doExport(ctx context.Context, export managedresource.Export) (map[string]interface{}, error) { - targetClient, err := lib.GetTargetClient(ctx, e.kubeClient, e.lsClient, e.deployItem, export.TargetName) + targetClient, err := lib.GetTargetClientConsideringSecondaryTarget(ctx, e.kubeClient, e.lsClient, e.deployItem, export.TargetName, e.lsRestConfig) if err != nil { return nil, err } diff --git a/pkg/deployer/lib/resourcemanager/objectapplier.go b/pkg/deployer/lib/resourcemanager/objectapplier.go index 4adf98c3d..8e72bd07b 100644 --- a/pkg/deployer/lib/resourcemanager/objectapplier.go +++ b/pkg/deployer/lib/resourcemanager/objectapplier.go @@ -22,6 +22,7 @@ import ( "k8s.io/apimachinery/pkg/types" apimacherrors "k8s.io/apimachinery/pkg/util/errors" "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/client" lsv1alpha1 "github.com/gardener/landscaper/apis/core/v1alpha1" @@ -72,6 +73,7 @@ type ManifestApplierOptions struct { InterruptionChecker interruption.InterruptionChecker LsUncachedClient client.Client + LsRestConfig *rest.Config } // ManifestApplier creates or updated manifest based on their definition. @@ -89,6 +91,7 @@ type ManifestApplier struct { deletionGroupsDuringUpdate []managedresource.DeletionGroupDefinition interruptionChecker interruption.InterruptionChecker lsUncachedClient client.Client + lsRestConfig *rest.Config // properties created during runtime @@ -140,6 +143,7 @@ func NewManifestApplier(opts ManifestApplierOptions) *ManifestApplier { interruptionChecker: opts.InterruptionChecker, apiResourceHandler: CreateApiResourceHandler(opts.Clientset), lsUncachedClient: opts.LsUncachedClient, + lsRestConfig: opts.LsRestConfig, } } @@ -449,6 +453,7 @@ func (a *ManifestApplier) cleanupOrphanedResourcesInGroups(ctx context.Context, a.kubeClient, a.deployItem, a.interruptionChecker, + a.lsRestConfig, ) } diff --git a/pkg/deployer/lib/target_access.go b/pkg/deployer/lib/target_access.go new file mode 100644 index 000000000..1d21ef14e --- /dev/null +++ b/pkg/deployer/lib/target_access.go @@ -0,0 +1,168 @@ +package lib + +import ( + "context" + "errors" + "fmt" + + authenticationv1 "k8s.io/api/authentication/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/yaml" + + lsv1alpha1 "github.com/gardener/landscaper/apis/core/v1alpha1" + "github.com/gardener/landscaper/apis/core/v1alpha1/targettypes" +) + +// defaultExpirationSeconds is the default expiration duration for tokens generated for OIDC Targets and Self Targets. +const defaultExpirationSeconds = 86400 // = 1 day + +// TargetAccess bundles the various objects to access a target cluster. +type TargetAccess struct { + targetClient client.Client + targetRestConfig *rest.Config + targetClientSet kubernetes.Interface +} + +func (ta *TargetAccess) TargetClient() client.Client { + return ta.targetClient +} + +func (ta *TargetAccess) TargetRestConfig() *rest.Config { + return ta.targetRestConfig +} + +func (ta *TargetAccess) TargetClientSet() kubernetes.Interface { + return ta.targetClientSet +} + +// NewTargetAccess constructs a TargetAccess, handling the different subtypes of kubernetes-cluster Targets, namely: +// - Targets with a kubeconfig, +// - OIDC Targets, and +// - Self Targets, i.e. Targets pointing to the resource cluster watched by the Landscaper. +func NewTargetAccess(ctx context.Context, resolvedTarget *lsv1alpha1.ResolvedTarget, + lsUncachedClient client.Client, lsRestConfig *rest.Config) (_ *TargetAccess, err error) { + + if resolvedTarget == nil { + return nil, errors.New("no target defined") + } + + if resolvedTarget.Target == nil { + return nil, fmt.Errorf("resolved target does not contain the original target") + } + + targetConfig := &targettypes.KubernetesClusterTargetConfig{} + if err := yaml.Unmarshal([]byte(resolvedTarget.Content), targetConfig); err != nil { + return nil, fmt.Errorf("unable to parse target confĂ­guration: %w", err) + } + + var restConfig *rest.Config + if targetConfig.Kubeconfig.StrVal != nil { + kubeconfigBytes := []byte(*targetConfig.Kubeconfig.StrVal) + restConfig, err = clientcmd.RESTConfigFromKubeConfig(kubeconfigBytes) + if err != nil { + return nil, fmt.Errorf("unable to create rest config from kubeconfig: %w", err) + } + + } else if targetConfig.OIDCConfig != nil { + restConfig, err = getRestConfigForOIDCTarget(ctx, targetConfig.OIDCConfig, resolvedTarget, lsUncachedClient) + if err != nil { + return nil, err + } + + } else if targetConfig.SelfConfig != nil { + restConfig, err = getRestConfigForSelfTarget(ctx, targetConfig.SelfConfig, resolvedTarget, lsUncachedClient, lsRestConfig) + if err != nil { + return nil, err + } + + } else { + return nil, fmt.Errorf("target contains neither kubeconfig, nor oidc config, nor self config") + } + + targetClient, err := client.New(restConfig, client.Options{}) + if err != nil { + return nil, err + } + + targetClientSet, err := kubernetes.NewForConfig(restConfig) + if err != nil { + return nil, err + } + + return &TargetAccess{ + targetClient: targetClient, + targetRestConfig: restConfig, + targetClientSet: targetClientSet, + }, nil +} + +func getRestConfigForOIDCTarget(ctx context.Context, oidcConfig *targettypes.OIDCConfig, resolvedTarget *lsv1alpha1.ResolvedTarget, lsUncachedClient client.Client) (*rest.Config, error) { + serviceAccount := &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: resolvedTarget.Namespace, + Name: oidcConfig.ServiceAccount.Name, + }, + } + + expirationSeconds := oidcConfig.ExpirationSeconds + if expirationSeconds == nil { + expirationSeconds = ptr.To[int64](defaultExpirationSeconds) + } + + tokenRequest := &authenticationv1.TokenRequest{ + Spec: authenticationv1.TokenRequestSpec{ + Audiences: oidcConfig.Audience, + ExpirationSeconds: expirationSeconds, + }, + } + + if err := lsUncachedClient.SubResource("token").Create(ctx, serviceAccount, tokenRequest); err != nil { + return nil, fmt.Errorf("unable to create token for oidc target: %w", err) + } + + return &rest.Config{ + Host: oidcConfig.Server, + BearerToken: tokenRequest.Status.Token, + TLSClientConfig: rest.TLSClientConfig{ + CAData: oidcConfig.CAData, + }, + }, nil +} + +func getRestConfigForSelfTarget(ctx context.Context, selfConfig *targettypes.SelfConfig, + resolvedTarget *lsv1alpha1.ResolvedTarget, lsUncachedClient client.Client, lsRestConfig *rest.Config) (*rest.Config, error) { + + serviceAccount := &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: resolvedTarget.Namespace, + Name: selfConfig.ServiceAccount.Name, + }, + } + + expirationSeconds := selfConfig.ExpirationSeconds + if expirationSeconds == nil { + expirationSeconds = ptr.To[int64](defaultExpirationSeconds) + } + + tokenRequest := &authenticationv1.TokenRequest{ + Spec: authenticationv1.TokenRequestSpec{ + ExpirationSeconds: expirationSeconds, + }, + } + + if err := lsUncachedClient.SubResource("token").Create(ctx, serviceAccount, tokenRequest); err != nil { + return nil, fmt.Errorf("unable to create token for self target: %w", err) + } + + return &rest.Config{ + Host: lsRestConfig.Host, + BearerToken: tokenRequest.Status.Token, + TLSClientConfig: lsRestConfig.TLSClientConfig, + }, nil +} diff --git a/pkg/deployer/lib/target_client_provider.go b/pkg/deployer/lib/target_client_provider.go index fd73b1848..d58e467e4 100644 --- a/pkg/deployer/lib/target_client_provider.go +++ b/pkg/deployer/lib/target_client_provider.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/client" lsv1alpha1 "github.com/gardener/landscaper/apis/core/v1alpha1" @@ -11,16 +12,17 @@ import ( "github.com/gardener/landscaper/pkg/utils/read_write_layer" ) -// GetTargetClient is used to determine the client to read resources for custom readiness checks, export collection, and deletion groups. -// Usually it is the client obtained from the Target of the DeployItem. +// GetTargetClientConsideringSecondaryTarget is used to determine the client to read resources for custom readiness checks, +// export collection, and deletion groups. Usually it is the client obtained from the Target of the DeployItem. // In some scenarios however, the Landscaper deploys an installer on a primary target cluster, and the installer // deploys the actual application on a secondary target cluster. -func GetTargetClient( +func GetTargetClientConsideringSecondaryTarget( ctx context.Context, primaryTargetClient client.Client, lsClient client.Client, deployItem *lsv1alpha1.DeployItem, - secondaryTargetName *string) (targetClient client.Client, err error) { + secondaryTargetName *string, + lsRestConfig *rest.Config) (targetClient client.Client, err error) { if secondaryTargetName == nil { return primaryTargetClient, nil @@ -49,10 +51,10 @@ func GetTargetClient( return nil, fmt.Errorf("unable to resolve secondary target %s: %w", *secondaryTargetName, err) } - _, targetClient, _, err = GetRestConfigAndClientAndClientSet(ctx, resolvedTarget, lsClient) + targetAccess, err := NewTargetAccess(ctx, resolvedTarget, lsClient, lsRestConfig) if err != nil { return nil, fmt.Errorf("unable to get secondary target client %s: %w", *secondaryTargetName, err) } - return targetClient, nil + return targetAccess.TargetClient(), nil } diff --git a/pkg/deployer/lib/utils.go b/pkg/deployer/lib/utils.go index 415b282c5..626915cda 100644 --- a/pkg/deployer/lib/utils.go +++ b/pkg/deployer/lib/utils.go @@ -7,27 +7,18 @@ package lib import ( "context" "encoding/json" - "errors" "fmt" "reflect" - authenticationv1 "k8s.io/api/authentication/v1" - "k8s.io/utils/ptr" - corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes" - "k8s.io/client-go/rest" - "k8s.io/client-go/tools/clientcmd" "k8s.io/client-go/tools/record" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" - "sigs.k8s.io/yaml" lsv1alpha1 "github.com/gardener/landscaper/apis/core/v1alpha1" lsv1alpha1helper "github.com/gardener/landscaper/apis/core/v1alpha1/helper" - "github.com/gardener/landscaper/apis/core/v1alpha1/targettypes" lserrors "github.com/gardener/landscaper/apis/errors" kutil "github.com/gardener/landscaper/controller-utils/pkg/kubernetes" "github.com/gardener/landscaper/controller-utils/pkg/landscaper/targetresolver" @@ -39,92 +30,6 @@ import ( "github.com/gardener/landscaper/pkg/utils/read_write_layer" ) -func GetRestConfigAndClientAndClientSet(ctx context.Context, resolvedTarget *lsv1alpha1.ResolvedTarget, lsUncachedClient client.Client) (_ *rest.Config, _ client.Client, _ kubernetes.Interface, err error) { - var restConfig *rest.Config - - if resolvedTarget.Target == nil { - return nil, nil, nil, fmt.Errorf("resolved target does not contain the original target") - } - - targetConfig := &targettypes.KubernetesClusterTargetConfig{} - if err := yaml.Unmarshal([]byte(resolvedTarget.Content), targetConfig); err != nil { - return nil, nil, nil, fmt.Errorf("unable to parse target confĂ­guration: %w", err) - } - - if targetConfig.Kubeconfig.StrVal != nil { - kubeconfigBytes, err := GetKubeconfigFromTargetConfig(targetConfig) - if err != nil { - return nil, nil, nil, err - } - - kubeconfig, err := clientcmd.NewClientConfigFromBytes(kubeconfigBytes) - if err != nil { - return nil, nil, nil, err - } - - restConfig, err = kubeconfig.ClientConfig() - if err != nil { - return nil, nil, nil, err - } - - } else if targetConfig.OIDCConfig != nil { - serviceAccount := &corev1.ServiceAccount{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: resolvedTarget.Namespace, - Name: targetConfig.OIDCConfig.ServiceAccount.Name, - }, - } - - expirationSeconds := targetConfig.OIDCConfig.ExpirationSeconds - if expirationSeconds == nil { - // use 1 day as default - expirationSeconds = ptr.To[int64](86400) - } - - tokenRequest := &authenticationv1.TokenRequest{ - Spec: authenticationv1.TokenRequestSpec{ - Audiences: targetConfig.OIDCConfig.Audience, - ExpirationSeconds: expirationSeconds, - }, - } - - if err = lsUncachedClient.SubResource("token").Create(ctx, serviceAccount, tokenRequest); err != nil { - return nil, nil, nil, fmt.Errorf("unable to create token: %w", err) - } - - restConfig = &rest.Config{ - Host: targetConfig.OIDCConfig.Server, - BearerToken: tokenRequest.Status.Token, - TLSClientConfig: rest.TLSClientConfig{ - CAData: targetConfig.OIDCConfig.CAData, - }, - } - - } else { - return nil, nil, nil, fmt.Errorf("unable build rest config from resolved target") - } - - kubeClient, err := client.New(restConfig, client.Options{}) - if err != nil { - return nil, nil, nil, err - } - clientset, err := kubernetes.NewForConfig(restConfig) - if err != nil { - return nil, nil, nil, err - } - - return restConfig, kubeClient, clientset, nil -} - -// GetKubeconfigFromTargetConfig fetches the kubeconfig from a given config. -// If the config defines the target from a secret that secret is read from all provided clients. -func GetKubeconfigFromTargetConfig(config *targettypes.KubernetesClusterTargetConfig) ([]byte, error) { - if config.Kubeconfig.StrVal != nil { - return []byte(*config.Kubeconfig.StrVal), nil - } - return nil, errors.New("kubeconfig not defined") -} - // CreateOrUpdateExport creates or updates the export of a deploy item. func CreateOrUpdateExport(ctx context.Context, kubeWriter *read_write_layer.Writer, kubeClient client.Client, deployItem *lsv1alpha1.DeployItem, values interface{}) error { if values == nil { diff --git a/pkg/deployer/manifest/add.go b/pkg/deployer/manifest/add.go index 9cafa64eb..187e97f53 100644 --- a/pkg/deployer/manifest/add.go +++ b/pkg/deployer/manifest/add.go @@ -39,7 +39,7 @@ func AddDeployerToManager(lsUncachedClient, lsCachedClient, hostUncachedClient, } log.Info("access to critical problems allowed") - d, err := NewDeployer(lsUncachedClient, lsCachedClient, hostUncachedClient, hostCachedClient, + d, err := NewDeployer(lsMgr.GetConfig(), lsUncachedClient, lsCachedClient, hostUncachedClient, hostCachedClient, log, config, ) diff --git a/pkg/deployer/manifest/controller.go b/pkg/deployer/manifest/controller.go index 29a48a0c2..56cdba1c5 100644 --- a/pkg/deployer/manifest/controller.go +++ b/pkg/deployer/manifest/controller.go @@ -8,13 +8,13 @@ import ( "context" "time" - manifestv1alpha2 "github.com/gardener/landscaper/apis/deployer/manifest/v1alpha2" - "github.com/gardener/landscaper/controller-utils/pkg/logging" - + "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/client" lsv1alpha1 "github.com/gardener/landscaper/apis/core/v1alpha1" + manifestv1alpha2 "github.com/gardener/landscaper/apis/deployer/manifest/v1alpha2" crval "github.com/gardener/landscaper/apis/deployer/utils/continuousreconcile/validation" + "github.com/gardener/landscaper/controller-utils/pkg/logging" deployerlib "github.com/gardener/landscaper/pkg/deployer/lib" cr "github.com/gardener/landscaper/pkg/deployer/lib/continuousreconcile" "github.com/gardener/landscaper/pkg/deployer/lib/extension" @@ -30,11 +30,13 @@ const ( ) // NewDeployer creates a new deployer that reconciles deploy items of type helm. -func NewDeployer(lsUncachedClient, lsCachedClient, hostUncachedClient, hostCachedClient client.Client, +func NewDeployer(lsRestConfig *rest.Config, + lsUncachedClient, lsCachedClient, hostUncachedClient, hostCachedClient client.Client, log logging.Logger, config manifestv1alpha2.Configuration) (deployerlib.Deployer, error) { dep := &deployer{ + lsRestConfig: lsRestConfig, lsUncachedClient: lsUncachedClient, lsCachedClient: lsCachedClient, hostUncachedClient: hostUncachedClient, @@ -48,6 +50,7 @@ func NewDeployer(lsUncachedClient, lsCachedClient, hostUncachedClient, hostCache } type deployer struct { + lsRestConfig *rest.Config lsUncachedClient client.Client lsCachedClient client.Client hostUncachedClient client.Client @@ -62,6 +65,7 @@ func (d *deployer) Reconcile(ctx context.Context, _ *lsv1alpha1.Context, di *lsv if err != nil { return err } + manifest.SetLsRestConfig(d.lsRestConfig) return manifest.Reconcile(ctx) } @@ -70,6 +74,7 @@ func (d deployer) Delete(ctx context.Context, _ *lsv1alpha1.Context, di *lsv1alp if err != nil { return err } + manifest.SetLsRestConfig(d.lsRestConfig) return manifest.Delete(ctx) } @@ -87,6 +92,7 @@ func (d *deployer) NextReconcile(ctx context.Context, last time.Time, di *lsv1al if err != nil { return nil, err } + manifest.SetLsRestConfig(d.lsRestConfig) if crval.ContinuousReconcileSpecIsEmpty(manifest.ProviderConfiguration.ContinuousReconcile) { // no continuous reconciliation configured return nil, nil diff --git a/pkg/deployer/manifest/ensure.go b/pkg/deployer/manifest/ensure.go index 9f5fcfcf4..16334c7b5 100644 --- a/pkg/deployer/manifest/ensure.go +++ b/pkg/deployer/manifest/ensure.go @@ -39,10 +39,8 @@ func (m *Manifest) Reconcile(ctx context.Context) error { m.DeployItem.Status.Phase = lsv1alpha1.DeployItemPhases.Progressing - _, targetClient, targetClientSet, err := m.TargetClient(ctx) - if err != nil { - return lserrors.NewWrappedError(err, - currOp, "TargetClusterClient", err.Error()) + if err := m.ensureTargetAccess(ctx); err != nil { + return lserrors.NewWrappedError(err, currOp, "ensureTargetAccess", err.Error()) } if m.ProviderStatus == nil { @@ -57,8 +55,8 @@ func (m *Manifest) Reconcile(ctx context.Context) error { applier := resourcemanager.NewManifestApplier(resourcemanager.ManifestApplierOptions{ Decoder: serializer.NewCodecFactory(Scheme).UniversalDecoder(), - KubeClient: targetClient, - Clientset: targetClientSet, + KubeClient: m.targetAccess.TargetClient(), + Clientset: m.targetAccess.TargetClientSet(), DeployItemName: m.DeployItem.Name, DeployItem: m.DeployItem, UpdateStrategy: m.ProviderConfiguration.UpdateStrategy, @@ -70,6 +68,7 @@ func (m *Manifest) Reconcile(ctx context.Context) error { DeletionGroupsDuringUpdate: m.ProviderConfiguration.DeletionGroupsDuringUpdate, InterruptionChecker: interruption.NewStandardInterruptionChecker(m.DeployItem, m.lsUncachedClient), LsUncachedClient: m.lsUncachedClient, + LsRestConfig: m.lsRestConfig, }) patchInfos, err := applier.Apply(ctx) @@ -97,7 +96,7 @@ func (m *Manifest) Reconcile(ctx context.Context) error { return err } - if err := m.CheckResourcesReady(ctx, targetClient); err != nil { + if err := m.CheckResourcesReady(ctx, m.targetAccess.TargetClient()); err != nil { return err } @@ -107,10 +106,11 @@ func (m *Manifest) Reconcile(ctx context.Context) error { } opts := resourcemanager.ExporterOptions{ - KubeClient: targetClient, + KubeClient: m.targetAccess.TargetClient(), InterruptionChecker: interruption.NewStandardInterruptionChecker(m.DeployItem, m.lsUncachedClient), LsClient: m.lsUncachedClient, DeployItem: m.DeployItem, + LsRestConfig: m.lsRestConfig, } exporter := resourcemanager.NewExporter(opts) @@ -175,6 +175,7 @@ func (m *Manifest) CheckResourcesReady(ctx context.Context, client client.Client InterruptionChecker: interruption.NewStandardInterruptionChecker(m.DeployItem, m.lsUncachedClient), LsClient: m.lsUncachedClient, DeployItem: m.DeployItem, + LsRestConfig: m.lsRestConfig, } err := customReadinessCheck.CheckResourcesReady(ctx) if err != nil { @@ -205,9 +206,8 @@ func (m *Manifest) deleteManifestsInGroups(ctx context.Context) error { return err } - _, targetClient, _, err := m.TargetClient(ctx) - if err != nil { - return lserrors.NewWrappedError(err, op, "TargetClusterClient", err.Error()) + if err := m.ensureTargetAccess(ctx); err != nil { + return lserrors.NewWrappedError(err, op, "ensureTargetAccess", err.Error()) } managedResources := []managedresource.ManagedResourceStatus{} @@ -219,7 +219,7 @@ func (m *Manifest) deleteManifestsInGroups(ctx context.Context) error { lc.KeyResourceKind, mr.Resource.Kind) mrLogger.Debug("Checking resource") - ok, err := resourcemanager.FilterByPolicy(mrCtx, mr, targetClient, m.DeployItem.Name) + ok, err := resourcemanager.FilterByPolicy(mrCtx, mr, m.targetAccess.TargetClient(), m.DeployItem.Name) if err != nil { return err } @@ -227,7 +227,7 @@ func (m *Manifest) deleteManifestsInGroups(ctx context.Context) error { continue } - notFound, err := resourcemanager.AnnotateAndPatchBeforeDelete(ctx, mr, targetClient) + notFound, err := resourcemanager.AnnotateAndPatchBeforeDelete(ctx, mr, m.targetAccess.TargetClient()) if err != nil { return err } @@ -241,14 +241,15 @@ func (m *Manifest) deleteManifestsInGroups(ctx context.Context) error { interruptionChecker := interruption.NewStandardInterruptionChecker(m.DeployItem, m.lsUncachedClient) - err = resourcemanager.DeleteManagedResources( + err := resourcemanager.DeleteManagedResources( ctx, m.lsUncachedClient, managedResources, m.ProviderConfiguration.DeletionGroups, - targetClient, + m.targetAccess.TargetClient(), m.DeployItem, interruptionChecker, + m.lsRestConfig, ) if err != nil { return fmt.Errorf("failed deleting managed resources: %w", err) diff --git a/pkg/deployer/manifest/manifest.go b/pkg/deployer/manifest/manifest.go index 7a20f767c..736b66cf4 100644 --- a/pkg/deployer/manifest/manifest.go +++ b/pkg/deployer/manifest/manifest.go @@ -6,25 +6,19 @@ package manifest import ( "context" - "errors" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/client" - lserrors "github.com/gardener/landscaper/apis/errors" - - "github.com/gardener/landscaper/pkg/deployer/lib" - - "github.com/gardener/landscaper/pkg/utils" - lsv1alpha1 "github.com/gardener/landscaper/apis/core/v1alpha1" manifestinstall "github.com/gardener/landscaper/apis/deployer/manifest/install" manifestv1alpha2 "github.com/gardener/landscaper/apis/deployer/manifest/v1alpha2" - manifestvalidation "github.com/gardener/landscaper/apis/deployer/manifest/validation" + lserrors "github.com/gardener/landscaper/apis/errors" "github.com/gardener/landscaper/pkg/api" + "github.com/gardener/landscaper/pkg/deployer/lib" + "github.com/gardener/landscaper/pkg/utils" ) const ( @@ -40,6 +34,7 @@ func init() { // Manifest is the internal representation of a DeployItem of Type Manifest type Manifest struct { + lsRestConfig *rest.Config lsUncachedClient client.Client hostUncachedClient client.Client @@ -50,9 +45,7 @@ type Manifest struct { ProviderConfiguration *manifestv1alpha2.ProviderConfiguration ProviderStatus *manifestv1alpha2.ProviderStatus - TargetKubeClient client.Client - TargetRestConfig *rest.Config - TargetClientSet kubernetes.Interface + targetAccess *lib.TargetAccess } // NewDeployItemBuilder creates a new deployitem builder for manifest deployitems @@ -101,21 +94,13 @@ func New(lsUncachedClient client.Client, hostUncachedClient client.Client, }, nil } -func (m *Manifest) TargetClient(ctx context.Context) (*rest.Config, client.Client, kubernetes.Interface, error) { - if m.TargetKubeClient != nil { - return m.TargetRestConfig, m.TargetKubeClient, m.TargetClientSet, nil - } - if m.Target != nil { - restConfig, kubeClient, clientset, err := lib.GetRestConfigAndClientAndClientSet(ctx, m.Target, m.lsUncachedClient) - if err != nil { - return nil, nil, nil, err - } - - m.TargetRestConfig = restConfig - m.TargetKubeClient = kubeClient - m.TargetClientSet = clientset +func (m *Manifest) SetLsRestConfig(lsRestConfig *rest.Config) { + m.lsRestConfig = lsRestConfig +} - return restConfig, kubeClient, clientset, nil +func (m *Manifest) ensureTargetAccess(ctx context.Context) (err error) { + if m.targetAccess == nil { + m.targetAccess, err = lib.NewTargetAccess(ctx, m.Target, m.lsUncachedClient, m.lsRestConfig) } - return nil, nil, nil, errors.New("neither a target nor kubeconfig are defined") + return err } diff --git a/pkg/deployer/manifest/test/e2e_test.go b/pkg/deployer/manifest/test/e2e_test.go index b83b813df..7e3a6ea2d 100644 --- a/pkg/deployer/manifest/test/e2e_test.go +++ b/pkg/deployer/manifest/test/e2e_test.go @@ -52,13 +52,13 @@ var _ = Describe("Manifest Deployer", func() { state, err = testenv.InitState(context.TODO()) Expect(err).ToNot(HaveOccurred()) - deployer, err := manifestctlr.NewDeployer(testenv.Client, testenv.Client, testenv.Client, testenv.Client, + deployer, err := manifestctlr.NewDeployer(nil, testenv.Client, testenv.Client, testenv.Client, testenv.Client, logging.Discard(), manifestv1alpha2.Configuration{}, ) Expect(err).ToNot(HaveOccurred()) - ctrl = deployerlib.NewController( + ctrl = deployerlib.NewController(nil, testenv.Client, testenv.Client, testenv.Client, testenv.Client, utils.NewFinishedObjectCache(), api.LandscaperScheme, diff --git a/pkg/deployer/mock/add.go b/pkg/deployer/mock/add.go index 0a16168dc..fd23f1bdb 100644 --- a/pkg/deployer/mock/add.go +++ b/pkg/deployer/mock/add.go @@ -71,7 +71,8 @@ func NewController(lsUncachedClient, lsCachedClient, hostUncachedClient, hostCac return nil, err } - return deployerlib.NewController(lsUncachedClient, lsCachedClient, hostUncachedClient, hostCachedClient, + return deployerlib.NewController(nil, + lsUncachedClient, lsCachedClient, hostUncachedClient, hostCachedClient, finishedObjectCache, scheme, eventRecorder, scheme, deployerlib.DeployerArgs{ diff --git a/test/integration/suite_test.go b/test/integration/suite_test.go index d05939178..ebf348b1e 100644 --- a/test/integration/suite_test.go +++ b/test/integration/suite_test.go @@ -9,8 +9,14 @@ import ( "flag" "testing" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/gardener/landscaper/hack/testcluster/pkg/utils" + "github.com/gardener/landscaper/test/framework" "github.com/gardener/landscaper/test/integration/core" "github.com/gardener/landscaper/test/integration/dependencies" + "github.com/gardener/landscaper/test/integration/deployers" "github.com/gardener/landscaper/test/integration/deployitems" "github.com/gardener/landscaper/test/integration/executions" "github.com/gardener/landscaper/test/integration/importexport" @@ -21,13 +27,6 @@ import ( "github.com/gardener/landscaper/test/integration/targets" "github.com/gardener/landscaper/test/integration/tutorial" "github.com/gardener/landscaper/test/integration/webhook" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - "github.com/gardener/landscaper/hack/testcluster/pkg/utils" - "github.com/gardener/landscaper/test/framework" - "github.com/gardener/landscaper/test/integration/deployers" ) var opts *framework.Options diff --git a/test/integration/targets/oidc_targets.go b/test/integration/targets/oidc_targets.go index fcadc5711..8b779ee13 100644 --- a/test/integration/targets/oidc_targets.go +++ b/test/integration/targets/oidc_targets.go @@ -2,9 +2,7 @@ package targets import ( "context" - "encoding/json" - "fmt" - "path" + "encoding/base64" "path/filepath" "time" @@ -14,10 +12,8 @@ import ( v12 "k8s.io/api/rbac/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/utils/ptr" lsv1alpha1 "github.com/gardener/landscaper/apis/core/v1alpha1" - "github.com/gardener/landscaper/apis/core/v1alpha1/targettypes" kutil "github.com/gardener/landscaper/controller-utils/pkg/kubernetes" lsutils "github.com/gardener/landscaper/pkg/utils/landscaper" "github.com/gardener/landscaper/test/framework" @@ -28,156 +24,75 @@ func OIDCTargetTests(ctx context.Context, f *framework.Framework) { Describe("OIDC Targets", func() { - const ( - openIDConnectApiVersion = "authentication.gardener.cloud/v1alpha1" - openIDConnectKind = "OpenIDConnect" - ) - var ( testdataDir = filepath.Join(f.RootPath, "test", "integration", "testdata", "targets", "oidc-targets") state = f.Register() ) - createOpenIDConnect := func(name, clientID, issuerURL, prefix string) (*unstructured.Unstructured, error) { - unstr := &unstructured.Unstructured{} - unstr.SetUnstructuredContent(map[string]interface{}{ - "spec": map[string]interface{}{ - "clientID": clientID, - "issuerURL": issuerURL, - "supportedSigningAlgs": []string{"RS256"}, - "usernameClaim": "sub", - "usernamePrefix": prefix, - }, - }) - unstr.SetAPIVersion(openIDConnectApiVersion) - unstr.SetKind(openIDConnectKind) - unstr.SetName(name) - err := f.Client.Create(ctx, unstr) - return unstr, err - } - - deleteOpenIDConnect := func(name string) error { - unstr := &unstructured.Unstructured{} - unstr.SetAPIVersion(openIDConnectApiVersion) - unstr.SetKind(openIDConnectKind) - unstr.SetName(name) - return f.Client.Delete(ctx, unstr) - } - - createAdminClusterRoleBinding := func(name, saName, saNamespace, prefix string) error { - b := &v12.ClusterRoleBinding{ - ObjectMeta: metav1.ObjectMeta{}, - Subjects: nil, - RoleRef: v12.RoleRef{}, - } - b.SetName(name) - b.RoleRef = v12.RoleRef{ - APIGroup: "rbac.authorization.k8s.io", - Kind: "ClusterRole", - Name: "cluster-admin", - } - b.Subjects = []v12.Subject{ - { - APIGroup: "rbac.authorization.k8s.io", - Kind: "User", - Name: fmt.Sprintf("%ssystem:serviceaccount:%s:%s", prefix, saNamespace, saName), - }, - } - - return f.Client.Create(ctx, b) - } - - createOIDCTarget := func(ctx context.Context, name, namespace, saName, audience string) (*lsv1alpha1.Target, error) { - config := &targettypes.KubernetesClusterTargetConfig{ - OIDCConfig: &targettypes.OIDCConfig{ - Server: f.RestConfig.Host, - CAData: f.RestConfig.CAData, - ServiceAccount: v1.LocalObjectReference{ - Name: saName, - }, - Audience: []string{audience}, - ExpirationSeconds: ptr.To[int64](86400), - }, - } - configRaw, err := json.Marshal(config) - if err != nil { - return nil, err - } - - t := &lsv1alpha1.Target{} - t.SetName(name) - t.SetNamespace(namespace) - t.Spec = lsv1alpha1.TargetSpec{ - Type: targettypes.KubernetesClusterTargetType, - Configuration: &lsv1alpha1.AnyJSON{ - RawMessage: configRaw, - }, - } - - if err := state.Create(ctx, t); err != nil { - return nil, err - } - return t, nil - } - It("should use an oidc target", func() { const ( - openIDConnectName = "resource-cluster-oidc" - targetName = "my-cluster-oidc" - serviceAccountName = "service-account-oidc" - bindingName = "binding-oidc" - audience = "target-cluster-oidc" - prefix = "resource-cluster-oidc:" + audience = "oidc-target-cluster" + configMapName = "oidc-target-test" ) + settings := map[string]any{ + "namespace": state.Namespace, + "openIDConnectName": "landscaper-integration-test-oidc-targets", + "clusterRoleBindingName": "landscaper:integration-test:oidc-targets", + "serviceAccountName": "oidc-serviceaccount", + "targetName": "oidc-target", + "installationName": "oidc-inst", + "configMapName": configMapName, + "audience": audience, + "clientID": audience, + "issuerURL": f.OIDCIssuerURL, + "server": f.RestConfig.Host, + "caData": base64.StdEncoding.EncodeToString(f.RestConfig.CAData), + "prefix": "resource-cluster-oidc:", + } + By("Create OpenIDConnect resource so that the target cluster trusts the resource cluster") - _, err := createOpenIDConnect(openIDConnectName, audience, f.OIDCIssuerURL, prefix) - Expect(err).NotTo(HaveOccurred()) + openIDConnect := &unstructured.Unstructured{} + Expect(utils.CreateClientObjectFromTemplate(ctx, f.Client, filepath.Join(testdataDir, "openidconnect.yaml"), settings, openIDConnect)).To(Succeed()) - By("Create ClusterRoleBinding on target cluster for ServiceAccount on resource cluster") - err = createAdminClusterRoleBinding(bindingName, serviceAccountName, state.Namespace, prefix) - Expect(err).NotTo(HaveOccurred()) + By("Create ClusterRoleBinding on resource cluster") + clusterRoleBinding := &v12.ClusterRoleBinding{} + Expect(utils.CreateClientObjectFromTemplate(ctx, f.Client, filepath.Join(testdataDir, "clusterrolebinding.yaml"), settings, clusterRoleBinding)).To(Succeed()) By("Create ServiceAccount on resource cluster") - _, err = utils.CreateServiceAccount(ctx, state.State, serviceAccountName, state.Namespace) - Expect(err).NotTo(HaveOccurred()) + serviceAccount := &v1.ServiceAccount{} + Expect(utils.CreateStateObjectFromTemplate(ctx, state.State, filepath.Join(testdataDir, "serviceaccount.yaml"), settings, serviceAccount)).To(Succeed()) - By("Create oidc target on resource cluster") - target, err := createOIDCTarget(ctx, targetName, state.Namespace, serviceAccountName, audience) - Expect(err).NotTo(HaveOccurred()) - - By("Create DataObject for namespace import") - doNamespace := &lsv1alpha1.DataObject{} - utils.ExpectNoError(utils.CreateNamespaceDataObjectFromFile(ctx, state.State, doNamespace, path.Join(testdataDir, "import-do-namespace.yaml"))) + By("Create OIDC Target on resource cluster") + target := &lsv1alpha1.Target{} + Expect(utils.CreateStateObjectFromTemplate(ctx, state.State, filepath.Join(testdataDir, "target.yaml"), settings, target)).To(Succeed()) By("Create Installation") inst := &lsv1alpha1.Installation{} - utils.ExpectNoError(utils.CreateInstallationFromFile(ctx, state.State, inst, path.Join(testdataDir, "installation.yaml"))) + Expect(utils.CreateStateObjectFromTemplate(ctx, state.State, filepath.Join(testdataDir, "installation.yaml"), settings, inst)).To(Succeed()) By("Wait for Installation to finish") utils.ExpectNoError(lsutils.WaitForInstallationToFinish(ctx, f.Client, inst, lsv1alpha1.InstallationPhases.Succeeded, 2*time.Minute)) By("Check deployed configmaps") - cm := &v1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "cm-1", Namespace: state.Namespace}} - key := kutil.ObjectKeyFromObject(cm) - Expect(f.Client.Get(ctx, key, cm)).To(Succeed()) + cm := &v1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: configMapName, Namespace: state.Namespace}} + Expect(f.Client.Get(ctx, kutil.ObjectKeyFromObject(cm), cm)).To(Succeed()) By("Delete installation") Expect(state.Client.Delete(ctx, inst)).To(Succeed()) Expect(lsutils.WaitForInstallationToBeDeleted(ctx, f.Client, inst, 2*time.Minute)).To(Succeed()) - By("Delete DataObject") - Expect(state.Client.Delete(ctx, doNamespace)).To(Succeed()) - By("Delete Target") Expect(state.Client.Delete(ctx, target)).To(Succeed()) By("Delete ServiceAccount") - Expect(utils.DeleteServiceAccount(ctx, state.State, serviceAccountName, state.Namespace)).To(Succeed()) + Expect(state.Client.Delete(ctx, serviceAccount)).To(Succeed()) + + By("Delete ClusterRoleBinding") + Expect(f.Client.Delete(ctx, clusterRoleBinding)).To(Succeed()) By("Delete OpenIDConnect resource") - Expect(deleteOpenIDConnect(openIDConnectName)).To(Succeed()) + Expect(f.Client.Delete(ctx, openIDConnect)).To(Succeed()) }) - }) } diff --git a/test/integration/targets/register.go b/test/integration/targets/register.go index c4915036d..f7c440d6b 100644 --- a/test/integration/targets/register.go +++ b/test/integration/targets/register.go @@ -18,4 +18,5 @@ func RegisterTests(f *framework.Framework) { TargetTests(f) TargetMapTests(ctx, f) OIDCTargetTests(ctx, f) + SelfTargetTests(ctx, f) } diff --git a/test/integration/targets/self_targets.go b/test/integration/targets/self_targets.go new file mode 100644 index 000000000..dbb74bd8c --- /dev/null +++ b/test/integration/targets/self_targets.go @@ -0,0 +1,81 @@ +package targets + +import ( + "context" + "path/filepath" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + v1 "k8s.io/api/core/v1" + v12 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + lsv1alpha1 "github.com/gardener/landscaper/apis/core/v1alpha1" + kutil "github.com/gardener/landscaper/controller-utils/pkg/kubernetes" + lsutils "github.com/gardener/landscaper/pkg/utils/landscaper" + "github.com/gardener/landscaper/test/framework" + "github.com/gardener/landscaper/test/utils" +) + +func SelfTargetTests(ctx context.Context, f *framework.Framework) { + + Describe("Self Targets", func() { + + var ( + testdataDir = filepath.Join(f.RootPath, "test", "integration", "testdata", "targets", "self-targets") + state = f.Register() + ) + + It("should use a self target", func() { + const ( + configMapName = "self-target-test" + ) + + settings := map[string]any{ + "namespace": state.Namespace, + "configMapName": configMapName, + "clusterRoleBindingName": "landscaper:integration-test:self-targets", + "installationName": "self-inst", + "serviceAccountName": "self-serviceaccount", + "targetName": "self-target", + } + + By("Create ServiceAccount on resource cluster") + serviceAccount := &v1.ServiceAccount{} + Expect(utils.CreateStateObjectFromTemplate(ctx, state.State, filepath.Join(testdataDir, "serviceaccount.yaml"), settings, serviceAccount)).To(Succeed()) + + By("Create ClusterRoleBinding on resource cluster") + clusterRoleBinding := &v12.ClusterRoleBinding{} + Expect(utils.CreateStateObjectFromTemplate(ctx, state.State, filepath.Join(testdataDir, "clusterrolebinding.yaml"), settings, clusterRoleBinding)).To(Succeed()) + + By("Create Self Target") + target := &lsv1alpha1.Target{} + Expect(utils.CreateStateObjectFromTemplate(ctx, state.State, filepath.Join(testdataDir, "target.yaml"), settings, target)).To(Succeed()) + + By("Create Installation") + inst := &lsv1alpha1.Installation{} + Expect(utils.CreateStateObjectFromTemplate(ctx, state.State, filepath.Join(testdataDir, "installation.yaml"), settings, inst)).To(Succeed()) + + By("Wait for Installation to finish") + utils.ExpectNoError(lsutils.WaitForInstallationToFinish(ctx, f.Client, inst, lsv1alpha1.InstallationPhases.Succeeded, 2*time.Minute)) + + By("Check deployed configmaps") + cm := &v1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: configMapName, Namespace: state.Namespace}} + Expect(f.Client.Get(ctx, kutil.ObjectKeyFromObject(cm), cm)).To(Succeed()) + + By("Delete installation") + Expect(state.Client.Delete(ctx, inst)).To(Succeed()) + Expect(lsutils.WaitForInstallationToBeDeleted(ctx, f.Client, inst, 2*time.Minute)).To(Succeed()) + + By("Delete Target") + Expect(state.Client.Delete(ctx, target)).To(Succeed()) + + By("Delete ServiceAccount") + Expect(state.Client.Delete(ctx, serviceAccount)).To(Succeed()) + + By("Delete ClusterRoleBinding") + Expect(state.Client.Delete(ctx, clusterRoleBinding)).To(Succeed()) + }) + }) +} diff --git a/test/integration/testdata/targets/oidc-targets/clusterrolebinding.yaml b/test/integration/testdata/targets/oidc-targets/clusterrolebinding.yaml new file mode 100644 index 000000000..76844400a --- /dev/null +++ b/test/integration/testdata/targets/oidc-targets/clusterrolebinding.yaml @@ -0,0 +1,12 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ .clusterRoleBindingName }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin +subjects: + - apiGroup: "rbac.authorization.k8s.io" + kind: User + name: {{ .prefix }}system:serviceaccount:{{ .namespace }}:{{ .serviceAccountName }} diff --git a/test/integration/testdata/targets/oidc-targets/import-do-namespace.yaml b/test/integration/testdata/targets/oidc-targets/import-do-namespace.yaml deleted file mode 100644 index 267f46c6d..000000000 --- a/test/integration/testdata/targets/oidc-targets/import-do-namespace.yaml +++ /dev/null @@ -1,6 +0,0 @@ -apiVersion: landscaper.gardener.cloud/v1alpha1 -kind: DataObject -metadata: - name: do-namespace - namespace: example -data: example diff --git a/test/integration/testdata/targets/oidc-targets/installation.yaml b/test/integration/testdata/targets/oidc-targets/installation.yaml index 44f333b36..3de6bdd21 100644 --- a/test/integration/testdata/targets/oidc-targets/installation.yaml +++ b/test/integration/testdata/targets/oidc-targets/installation.yaml @@ -3,19 +3,13 @@ kind: Installation metadata: annotations: landscaper.gardener.cloud/operation: reconcile - name: oidc-1 - namespace: example - + name: {{ .installationName }} + namespace: {{ .namespace }} spec: - imports: targets: - name: cluster - target: my-cluster-oidc - data: - - name: namespace - dataRef: do-namespace - + target: {{ .targetName }} blueprint: inline: filesystem: @@ -23,15 +17,9 @@ spec: apiVersion: landscaper.gardener.cloud/v1alpha1 kind: Blueprint jsonSchema: "https://json-schema.org/draft/2019-09/schema" - imports: - name: cluster targetType: landscaper.gardener.cloud/kubernetes-cluster - - name: namespace - type: data - schema: - type: string - deployExecutions: - name: default type: GoTemplate @@ -51,7 +39,7 @@ spec: apiVersion: v1 kind: ConfigMap metadata: - name: cm-1 - namespace: {{ .imports.namespace }} + name: {{ .configMapName }} + namespace: {{ .namespace }} data: foo: bar diff --git a/test/integration/testdata/targets/oidc-targets/openidconnect.yaml b/test/integration/testdata/targets/oidc-targets/openidconnect.yaml new file mode 100644 index 000000000..a5db88473 --- /dev/null +++ b/test/integration/testdata/targets/oidc-targets/openidconnect.yaml @@ -0,0 +1,11 @@ +apiVersion: authentication.gardener.cloud/v1alpha1 +kind: OpenIDConnect +metadata: + name: {{ .openIDConnectName }} +spec: + clientID: {{ .clientID }} + issuerURL: {{ .issuerURL }} + supportedSigningAlgs: + - RS256 + usernameClaim: sub + usernamePrefix: '{{ .prefix }}' diff --git a/test/integration/testdata/targets/oidc-targets/serviceaccount.yaml b/test/integration/testdata/targets/oidc-targets/serviceaccount.yaml new file mode 100644 index 000000000..af06bfdc6 --- /dev/null +++ b/test/integration/testdata/targets/oidc-targets/serviceaccount.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ .serviceAccountName }} + namespace: {{ .namespace }} diff --git a/test/integration/testdata/targets/oidc-targets/target.yaml b/test/integration/testdata/targets/oidc-targets/target.yaml new file mode 100644 index 000000000..ca983d026 --- /dev/null +++ b/test/integration/testdata/targets/oidc-targets/target.yaml @@ -0,0 +1,16 @@ +apiVersion: landscaper.gardener.cloud/v1alpha1 +kind: Target +metadata: + name: {{ .targetName }} + namespace: {{ .namespace }} +spec: + config: + oidcConfig: + server: {{ .server }} + caData: {{ .caData }} + audience: + - {{ .audience }} + serviceAccount: + name: {{ .serviceAccountName }} + expirationSeconds: 3600 + type: landscaper.gardener.cloud/kubernetes-cluster diff --git a/test/integration/testdata/targets/self-targets/clusterrolebinding.yaml b/test/integration/testdata/targets/self-targets/clusterrolebinding.yaml new file mode 100644 index 000000000..d410a22c7 --- /dev/null +++ b/test/integration/testdata/targets/self-targets/clusterrolebinding.yaml @@ -0,0 +1,12 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ .clusterRoleBindingName }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin +subjects: + - kind: ServiceAccount + name: {{ .serviceAccountName }} + namespace: {{ .namespace }} diff --git a/test/integration/testdata/targets/self-targets/installation.yaml b/test/integration/testdata/targets/self-targets/installation.yaml new file mode 100644 index 000000000..3de6bdd21 --- /dev/null +++ b/test/integration/testdata/targets/self-targets/installation.yaml @@ -0,0 +1,45 @@ +apiVersion: landscaper.gardener.cloud/v1alpha1 +kind: Installation +metadata: + annotations: + landscaper.gardener.cloud/operation: reconcile + name: {{ .installationName }} + namespace: {{ .namespace }} +spec: + imports: + targets: + - name: cluster + target: {{ .targetName }} + blueprint: + inline: + filesystem: + blueprint.yaml: | + apiVersion: landscaper.gardener.cloud/v1alpha1 + kind: Blueprint + jsonSchema: "https://json-schema.org/draft/2019-09/schema" + imports: + - name: cluster + targetType: landscaper.gardener.cloud/kubernetes-cluster + deployExecutions: + - name: default + type: GoTemplate + template: | + deployItems: + - name: item-1 + type: landscaper.gardener.cloud/kubernetes-manifest + target: + import: cluster + config: + apiVersion: manifest.deployer.landscaper.gardener.cloud/v1alpha2 + kind: ProviderConfiguration + updateStrategy: update + manifests: + - policy: manage + manifest: + apiVersion: v1 + kind: ConfigMap + metadata: + name: {{ .configMapName }} + namespace: {{ .namespace }} + data: + foo: bar diff --git a/test/integration/testdata/targets/self-targets/serviceaccount.yaml b/test/integration/testdata/targets/self-targets/serviceaccount.yaml new file mode 100644 index 000000000..af06bfdc6 --- /dev/null +++ b/test/integration/testdata/targets/self-targets/serviceaccount.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ .serviceAccountName }} + namespace: {{ .namespace }} diff --git a/test/integration/testdata/targets/self-targets/target.yaml b/test/integration/testdata/targets/self-targets/target.yaml new file mode 100644 index 000000000..6a71fa3fe --- /dev/null +++ b/test/integration/testdata/targets/self-targets/target.yaml @@ -0,0 +1,12 @@ +apiVersion: landscaper.gardener.cloud/v1alpha1 +kind: Target +metadata: + name: {{ .targetName }} + namespace: {{ .namespace }} +spec: + config: + selfConfig: + serviceAccount: + name: {{ .serviceAccountName }} + expirationSeconds: 3600 + type: landscaper.gardener.cloud/kubernetes-cluster diff --git a/test/utils/builder.go b/test/utils/builder.go index ee6614aab..a27db415a 100644 --- a/test/utils/builder.go +++ b/test/utils/builder.go @@ -1,16 +1,67 @@ package utils import ( + "bytes" "context" "fmt" + "os" + "text/template" k8sv1 "k8s.io/api/core/v1" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/yaml" lsv1alpha1 "github.com/gardener/landscaper/apis/core/v1alpha1" "github.com/gardener/landscaper/test/utils/envtest" ) +func BuildObjectFromTemplate(filePath string, settings map[string]any, obj client.Object) error { + data, err := os.ReadFile(filePath) + if err != nil { + return err + } + + tmpl, err := template.New("tmpl").Parse(string(data)) + if err != nil { + return err + } + + var w bytes.Buffer + if err := tmpl.Execute(&w, settings); err != nil { + return err + } + + if err := yaml.Unmarshal(w.Bytes(), obj); err != nil { + return err + } + + return nil +} + +func CreateStateObjectFromTemplate(ctx context.Context, state *envtest.State, filePath string, settings map[string]any, obj client.Object) error { + if err := BuildObjectFromTemplate(filePath, settings, obj); err != nil { + return err + } + + if err := state.Create(ctx, obj); err != nil { + return err + } + + return nil +} + +func CreateClientObjectFromTemplate(ctx context.Context, cl client.Client, filePath string, settings map[string]any, obj client.Object) error { + if err := BuildObjectFromTemplate(filePath, settings, obj); err != nil { + return err + } + + if err := cl.Create(ctx, obj); err != nil { + return err + } + + return nil +} + func CreateDataObjectFromFile(ctx context.Context, state *envtest.State, do *lsv1alpha1.DataObject, path string) error { if err := ReadResourceFromFile(do, path); err != nil { return err