diff --git a/Dockerfile b/Dockerfile index be8fb170..17c7b7ed 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,7 +14,6 @@ RUN go mod download COPY generated ./generated COPY cmd ./cmd COPY internal ./internal -COPY pkg ./pkg COPY tools ./tools COPY hack ./hack diff --git a/cmd/local-user-authenticator/main.go b/cmd/local-user-authenticator/main.go index 4616c65d..f774500c 100644 --- a/cmd/local-user-authenticator/main.go +++ b/cmd/local-user-authenticator/main.go @@ -310,6 +310,9 @@ func startControllers( apicerts.NewCertsManagerController( namespace, certsSecretResourceName, + map[string]string{ + "app": "local-user-authenticator", + }, kubeClient, kubeInformers.Core().V1().Secrets(), controllerlib.WithInformer, diff --git a/cmd/pinniped-supervisor/main.go b/cmd/pinniped-supervisor/main.go index 445c9461..6a3d5c4f 100644 --- a/cmd/pinniped-supervisor/main.go +++ b/cmd/pinniped-supervisor/main.go @@ -23,6 +23,7 @@ import ( pinnipedclientset "go.pinniped.dev/generated/1.19/client/clientset/versioned" pinnipedinformers "go.pinniped.dev/generated/1.19/client/informers/externalversions" + "go.pinniped.dev/internal/config/supervisor" "go.pinniped.dev/internal/controller/supervisorconfig" "go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/downward" @@ -63,6 +64,7 @@ func waitForSignal() os.Signal { func startControllers( ctx context.Context, + cfg *supervisor.Config, issuerProvider *manager.Manager, kubeClient kubernetes.Interface, pinnipedClient pinnipedclientset.Interface, @@ -84,6 +86,7 @@ func startControllers( ). WithController( supervisorconfig.NewJWKSController( + cfg.Labels, kubeClient, pinnipedClient, kubeInformers.Core().V1().Secrets(), @@ -120,7 +123,7 @@ func newClients() (kubernetes.Interface, pinnipedclientset.Interface, error) { return kubeClient, pinnipedClient, nil } -func run(serverInstallationNamespace string) error { +func run(serverInstallationNamespace string, cfg *supervisor.Config) error { ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -142,7 +145,7 @@ func run(serverInstallationNamespace string) error { ) oidProvidersManager := manager.NewManager(http.NotFoundHandler()) - startControllers(ctx, oidProvidersManager, kubeClient, pinnipedClient, kubeInformers, pinnipedInformers) + startControllers(ctx, cfg, oidProvidersManager, kubeClient, pinnipedClient, kubeInformers, pinnipedInformers) //nolint: gosec // Intentionally binding to all network interfaces. l, err := net.Listen("tcp", ":80") @@ -173,7 +176,13 @@ func main() { klog.Fatal(fmt.Errorf("could not read pod metadata: %w", err)) } - if err := run(podInfo.Namespace); err != nil { + // Read the server config file. + cfg, err := supervisor.FromPath(os.Args[2]) + if err != nil { + klog.Fatal(fmt.Errorf("could not load config: %w", err)) + } + + if err := run(podInfo.Namespace, cfg); err != nil { klog.Fatal(err) } } diff --git a/deploy/concierge/deployment.yaml b/deploy/concierge/deployment.yaml index 75cfd923..0a91e772 100644 --- a/deploy/concierge/deployment.yaml +++ b/deploy/concierge/deployment.yaml @@ -2,28 +2,31 @@ #! SPDX-License-Identifier: Apache-2.0 #@ load("@ytt:data", "data") +#@ load("@ytt:json", "json") +#@ load("helpers.lib.yaml", "defaultLabel", "labels", "namespace", "defaultResourceName", "defaultResourceNameWithSuffix") +#@ if not data.values.into_namespace: --- apiVersion: v1 kind: Namespace metadata: name: #@ data.values.namespace - labels: - name: #@ data.values.namespace + labels: #@ labels() +#@ end --- apiVersion: v1 kind: ServiceAccount metadata: - name: #@ data.values.app_name - namespace: #@ data.values.namespace + name: #@ defaultResourceName() + namespace: #@ namespace() + labels: #@ labels() --- apiVersion: v1 kind: ConfigMap metadata: - name: #@ data.values.app_name + "-config" - namespace: #@ data.values.namespace - labels: - app: #@ data.values.app_name + name: #@ defaultResourceNameWithSuffix("config") + namespace: #@ namespace() + labels: #@ labels() data: #! If names.apiService is changed in this ConfigMap, must also change name of the ClusterIP Service resource below. #@yaml/text-templated-strings @@ -35,11 +38,12 @@ data: durationSeconds: (@= str(data.values.api_serving_certificate_duration_seconds) @) renewBeforeSeconds: (@= str(data.values.api_serving_certificate_renew_before_seconds) @) names: - servingCertificateSecret: (@= data.values.app_name + "-api-tls-serving-certificate" @) - credentialIssuerConfig: (@= data.values.app_name + "-config" @) - apiService: (@= data.values.app_name + "-api" @) + servingCertificateSecret: (@= defaultResourceNameWithSuffix("api-tls-serving-certificate") @) + credentialIssuerConfig: (@= defaultResourceNameWithSuffix("config") @) + apiService: (@= defaultResourceNameWithSuffix("api") @) + labels: (@= json.encode(labels()).rstrip() @) kubeCertAgent: - namePrefix: (@= data.values.app_name + "-kube-cert-agent-" @) + namePrefix: (@= defaultResourceNameWithSuffix("kube-cert-agent-") @) (@ if data.values.kube_cert_agent_image: @) image: (@= data.values.kube_cert_agent_image @) (@ else: @) @@ -59,9 +63,8 @@ apiVersion: v1 kind: Secret metadata: name: image-pull-secret - namespace: #@ data.values.namespace - labels: - app: #@ data.values.app_name + namespace: #@ namespace() + labels: #@ labels() type: kubernetes.io/dockerconfigjson data: .dockerconfigjson: #@ data.values.image_pull_dockerconfigjson @@ -70,29 +73,26 @@ data: apiVersion: apps/v1 kind: Deployment metadata: - name: #@ data.values.app_name - namespace: #@ data.values.namespace - labels: - app: #@ data.values.app_name + name: #@ defaultResourceName() + namespace: #@ namespace() + labels: #@ labels() spec: replicas: #@ data.values.replicas selector: - matchLabels: - app: #@ data.values.app_name + matchLabels: #@ defaultLabel() template: metadata: - labels: - app: #@ data.values.app_name + labels: #@ defaultLabel() annotations: scheduler.alpha.kubernetes.io/critical-pod: "" spec: - serviceAccountName: #@ data.values.app_name + serviceAccountName: #@ defaultResourceName() #@ if data.values.image_pull_dockerconfigjson and data.values.image_pull_dockerconfigjson != "": imagePullSecrets: - name: image-pull-secret #@ end containers: - - name: pinniped + - name: #@ defaultResourceName() #@ if data.values.image_digest: image: #@ data.values.image_repo + "@" + data.values.image_digest #@ else: @@ -131,7 +131,7 @@ spec: volumes: - name: config-volume configMap: - name: #@ data.values.app_name + "-config" + name: #@ defaultResourceNameWithSuffix("config") - name: podinfo downwardAPI: items: @@ -157,22 +157,19 @@ spec: - weight: 50 podAffinityTerm: labelSelector: - matchLabels: - app: #@ data.values.app_name + matchLabels: #@ defaultLabel() topologyKey: kubernetes.io/hostname --- apiVersion: v1 kind: Service metadata: #! If name is changed, must also change names.apiService in the ConfigMap above and spec.service.name in the APIService below. - name: #@ data.values.app_name + "-api" - namespace: #@ data.values.namespace - labels: - app: #@ data.values.app_name + name: #@ defaultResourceNameWithSuffix("api") + namespace: #@ namespace() + labels: #@ labels() spec: type: ClusterIP - selector: - app: #@ data.values.app_name + selector: #@ defaultLabel() ports: - protocol: TCP port: 443 @@ -182,8 +179,7 @@ apiVersion: apiregistration.k8s.io/v1 kind: APIService metadata: name: v1alpha1.login.pinniped.dev - labels: - app: #@ data.values.app_name + labels: #@ labels() spec: version: v1alpha1 group: login.pinniped.dev @@ -191,6 +187,6 @@ spec: versionPriority: 10 #! caBundle: Do not include this key here. Starts out null, will be updated/owned by the golang code. service: - name: #@ data.values.app_name + "-api" - namespace: #@ data.values.namespace + name: #@ defaultResourceNameWithSuffix("api") + namespace: #@ namespace() port: 443 diff --git a/deploy/concierge/helpers.lib.yaml b/deploy/concierge/helpers.lib.yaml new file mode 100644 index 00000000..f893152e --- /dev/null +++ b/deploy/concierge/helpers.lib.yaml @@ -0,0 +1,30 @@ +#! Copyright 2020 the Pinniped contributors. All Rights Reserved. +#! SPDX-License-Identifier: Apache-2.0 + +#@ load("@ytt:data", "data") +#@ load("@ytt:template", "template") + +#@ def defaultResourceName(): +#@ return data.values.app_name +#@ end + +#@ def defaultResourceNameWithSuffix(suffix): +#@ return data.values.app_name + "-" + suffix +#@ end + +#@ def namespace(): +#@ if data.values.into_namespace: +#@ return data.values.into_namespace +#@ else: +#@ return data.values.namespace +#@ end +#@ end + +#@ def defaultLabel(): +app: #@ data.values.app_name +#@ end + +#@ def labels(): +_: #@ template.replace(defaultLabel()) +_: #@ template.replace(data.values.custom_labels) +#@ end diff --git a/deploy/concierge/rbac.yaml b/deploy/concierge/rbac.yaml index 99750b5c..2df79e31 100644 --- a/deploy/concierge/rbac.yaml +++ b/deploy/concierge/rbac.yaml @@ -2,35 +2,38 @@ #! SPDX-License-Identifier: Apache-2.0 #@ load("@ytt:data", "data") +#@ load("helpers.lib.yaml", "labels", "namespace", "defaultResourceName", "defaultResourceNameWithSuffix") #! Give permission to various cluster-scoped objects --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: - name: #@ data.values.app_name + "-aggregated-api-server" + name: #@ defaultResourceNameWithSuffix("aggregated-api-server") + labels: #@ labels() rules: - - apiGroups: [""] - resources: [namespaces] - verbs: [get, list, watch] - - apiGroups: [apiregistration.k8s.io] - resources: [apiservices] - verbs: [create, get, list, patch, update, watch] - - apiGroups: [admissionregistration.k8s.io] - resources: [validatingwebhookconfigurations, mutatingwebhookconfigurations] - verbs: [get, list, watch] + - apiGroups: [ "" ] + resources: [ namespaces ] + verbs: [ get, list, watch ] + - apiGroups: [ apiregistration.k8s.io ] + resources: [ apiservices ] + verbs: [ create, get, list, patch, update, watch ] + - apiGroups: [ admissionregistration.k8s.io ] + resources: [ validatingwebhookconfigurations, mutatingwebhookconfigurations ] + verbs: [ get, list, watch ] --- kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: - name: #@ data.values.app_name + "-aggregated-api-server" + name: #@ defaultResourceNameWithSuffix("aggregated-api-server") + labels: #@ labels() subjects: - kind: ServiceAccount - name: #@ data.values.app_name - namespace: #@ data.values.namespace + name: #@ defaultResourceName() + namespace: #@ namespace() roleRef: kind: ClusterRole - name: #@ data.values.app_name + "-aggregated-api-server" + name: #@ defaultResourceNameWithSuffix("aggregated-api-server") apiGroup: rbac.authorization.k8s.io #! Give permission to various objects within the app's own namespace @@ -38,39 +41,41 @@ roleRef: apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: - name: #@ data.values.app_name + "-aggregated-api-server" - namespace: #@ data.values.namespace + name: #@ defaultResourceNameWithSuffix("aggregated-api-server") + namespace: #@ namespace() + labels: #@ labels() rules: - - apiGroups: [""] - resources: [services] - verbs: [create, get, list, patch, update, watch] - - apiGroups: [""] - resources: [secrets] - verbs: [create, get, list, patch, update, watch, delete] + - apiGroups: [ "" ] + resources: [ services ] + verbs: [ create, get, list, patch, update, watch ] + - apiGroups: [ "" ] + resources: [ secrets ] + verbs: [ create, get, list, patch, update, watch, delete ] #! We need to be able to CRUD pods in our namespace so we can reconcile the kube-cert-agent pods. - - apiGroups: [""] - resources: [pods] - verbs: [create, get, list, patch, update, watch, delete] + - apiGroups: [ "" ] + resources: [ pods ] + verbs: [ create, get, list, patch, update, watch, delete ] #! We need to be able to exec into pods in our namespace so we can grab the API server's private key - - apiGroups: [""] - resources: [pods/exec] - verbs: [create] - - apiGroups: [config.pinniped.dev, idp.pinniped.dev] - resources: ["*"] - verbs: [create, get, list, update, watch] + - apiGroups: [ "" ] + resources: [ pods/exec ] + verbs: [ create ] + - apiGroups: [ config.pinniped.dev, idp.pinniped.dev ] + resources: [ "*" ] + verbs: [ create, get, list, update, watch ] --- kind: RoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: - name: #@ data.values.app_name + "-aggregated-api-server" - namespace: #@ data.values.namespace + name: #@ defaultResourceNameWithSuffix("aggregated-api-server") + namespace: #@ namespace() + labels: #@ labels() subjects: - kind: ServiceAccount - name: #@ data.values.app_name - namespace: #@ data.values.namespace + name: #@ defaultResourceName() + namespace: #@ namespace() roleRef: kind: Role - name: #@ data.values.app_name + "-aggregated-api-server" + name: #@ defaultResourceNameWithSuffix("aggregated-api-server") apiGroup: rbac.authorization.k8s.io #! Give permission to read pods in the kube-system namespace so we can find the API server's private key @@ -78,25 +83,27 @@ roleRef: apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: - name: #@ data.values.app_name + "-kube-system-pod-read" + name: #@ defaultResourceNameWithSuffix("kube-system-pod-read") namespace: kube-system + labels: #@ labels() rules: - - apiGroups: [""] - resources: [pods] - verbs: [get, list, watch] + - apiGroups: [ "" ] + resources: [ pods ] + verbs: [ get, list, watch ] --- kind: RoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: - name: #@ data.values.app_name + "-kube-system-pod-read" + name: #@ defaultResourceNameWithSuffix("kube-system-pod-read") namespace: kube-system + labels: #@ labels() subjects: - kind: ServiceAccount - name: #@ data.values.app_name - namespace: #@ data.values.namespace + name: #@ defaultResourceName() + namespace: #@ namespace() roleRef: kind: Role - name: #@ data.values.app_name + "-kube-system-pod-read" + name: #@ defaultResourceNameWithSuffix("kube-system-pod-read") apiGroup: rbac.authorization.k8s.io #! Allow both authenticated and unauthenticated TokenCredentialRequests (i.e. allow all requests) @@ -104,16 +111,18 @@ roleRef: apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: - name: #@ data.values.app_name + "-create-token-credential-requests" + name: #@ defaultResourceNameWithSuffix("create-token-credential-requests") + labels: #@ labels() rules: - - apiGroups: [login.pinniped.dev] - resources: [tokencredentialrequests] - verbs: [create] + - apiGroups: [ login.pinniped.dev ] + resources: [ tokencredentialrequests ] + verbs: [ create ] --- kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: - name: #@ data.values.app_name + "-create-token-credential-requests" + name: #@ defaultResourceNameWithSuffix("create-token-credential-requests") + labels: #@ labels() subjects: - kind: Group name: system:authenticated @@ -123,7 +132,7 @@ subjects: apiGroup: rbac.authorization.k8s.io roleRef: kind: ClusterRole - name: #@ data.values.app_name + "-create-token-credential-requests" + name: #@ defaultResourceNameWithSuffix("create-token-credential-requests") apiGroup: rbac.authorization.k8s.io #! Give permissions for subjectaccessreviews, tokenreview that is needed by aggregated api servers @@ -131,12 +140,13 @@ roleRef: kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: - name: #@ data.values.app_name - namespace: #@ data.values.namespace + name: #@ defaultResourceName() + namespace: #@ namespace() + labels: #@ labels() subjects: - kind: ServiceAccount - name: #@ data.values.app_name - namespace: #@ data.values.namespace + name: #@ defaultResourceName() + namespace: #@ namespace() roleRef: kind: ClusterRole name: system:auth-delegator @@ -147,12 +157,13 @@ roleRef: kind: RoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: - name: #@ data.values.app_name + "-extension-apiserver-authentication-reader" + name: #@ defaultResourceNameWithSuffix("extension-apiserver-authentication-reader") namespace: kube-system + labels: #@ labels() subjects: - kind: ServiceAccount - name: #@ data.values.app_name - namespace: #@ data.values.namespace + name: #@ defaultResourceName() + namespace: #@ namespace() roleRef: kind: Role name: extension-apiserver-authentication-reader @@ -163,23 +174,25 @@ roleRef: kind: Role apiVersion: rbac.authorization.k8s.io/v1 metadata: - name: #@ data.values.app_name + "-cluster-info-lister-watcher" + name: #@ defaultResourceNameWithSuffix("cluster-info-lister-watcher") namespace: kube-public + labels: #@ labels() rules: - - apiGroups: [""] - resources: [configmaps] - verbs: [list, watch] + - apiGroups: [ "" ] + resources: [ configmaps ] + verbs: [ list, watch ] --- kind: RoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: - name: #@ data.values.app_name + "-cluster-info-lister-watcher" + name: #@ defaultResourceNameWithSuffix("cluster-info-lister-watcher") namespace: kube-public + labels: #@ labels() subjects: - kind: ServiceAccount - name: #@ data.values.app_name - namespace: #@ data.values.namespace + name: #@ defaultResourceName() + namespace: #@ namespace() roleRef: kind: Role - name: #@ data.values.app_name + "-cluster-info-lister-watcher" + name: #@ defaultResourceNameWithSuffix("cluster-info-lister-watcher") apiGroup: rbac.authorization.k8s.io diff --git a/deploy/concierge/values.yaml b/deploy/concierge/values.yaml index 49271f0f..00eddbdb 100644 --- a/deploy/concierge/values.yaml +++ b/deploy/concierge/values.yaml @@ -5,7 +5,21 @@ --- app_name: pinniped-concierge + +#! Creates a new namespace statically in yaml with the given name and installs the app into that namespace. namespace: pinniped-concierge +#! If specified, assumes that a namespace of the given name already exists and installs the app into that namespace. +#! If both `namespace` and `into_namespace` are specified, then only `into_namespace` is used. +into_namespace: #! e.g. my-preexisting-namespace + +#! All resources created statically by yaml at install-time and all resources created dynamically +#! by controllers at runtime will be labelled with `app: $app_name` and also with the labels +#! specified here. The value of `custom_labels` must be a map of string keys to string values. +#! The app can be uninstalled either by: +#! 1. Deleting the static install-time yaml resources including the static namespace, which will cascade and also delete +#! resources that were dynamically created by controllers at runtime +#! 2. Or, deleting all resources by label, which does not assume that there was a static install-time yaml namespace. +custom_labels: {} #! e.g. {myCustomLabelName: myCustomLabelValue, otherCustomLabelName: otherCustomLabelValue} #! Specify how many replicas of the Pinniped server to run. replicas: 2 @@ -20,7 +34,7 @@ image_tag: latest #! By default, the same image specified for image_repo/image_digest/image_tag will be re-used. kube_cert_agent_image: -#! Specifies a secret to be used when pulling the above container image. +#! Specifies a secret to be used when pulling the above `image_repo` container image. #! Can be used when the above image_repo is a private registry. #! Typically the value would be the output of: kubectl create secret docker-registry x --docker-server=https://example.io --docker-username="USERNAME" --docker-password="PASSWORD" --dry-run=client -o json | jq -r '.data[".dockerconfigjson"]' #! Optional. diff --git a/deploy/concierge/z0_crd_overlay.yaml b/deploy/concierge/z0_crd_overlay.yaml new file mode 100644 index 00000000..dfece13b --- /dev/null +++ b/deploy/concierge/z0_crd_overlay.yaml @@ -0,0 +1,17 @@ +#! Copyright 2020 the Pinniped contributors. All Rights Reserved. +#! SPDX-License-Identifier: Apache-2.0 + +#@ load("@ytt:overlay", "overlay") +#@ load("helpers.lib.yaml", "labels") + +#@overlay/match by=overlay.subset({"kind": "CustomResourceDefinition", "metadata":{"name":"credentialissuerconfigs.config.pinniped.dev"}}), expects=1 +--- +metadata: + #@overlay/match missing_ok=True + labels: #@ labels() + +#@overlay/match by=overlay.subset({"kind": "CustomResourceDefinition", "metadata":{"name":"webhookidentityproviders.idp.pinniped.dev"}}), expects=1 +--- +metadata: + #@overlay/match missing_ok=True + labels: #@ labels() diff --git a/deploy/local-user-authenticator/values.yaml b/deploy/local-user-authenticator/values.yaml index 9d4b96ed..19c6f237 100644 --- a/deploy/local-user-authenticator/values.yaml +++ b/deploy/local-user-authenticator/values.yaml @@ -9,7 +9,7 @@ image_repo: docker.io/getpinniped/pinniped-server image_digest: #! e.g. sha256:f3c4fdfd3ef865d4b97a1fd295d94acc3f0c654c46b6f27ffad5cf80216903c8 image_tag: latest -#! Specifies a secret to be used when pulling the above container image. +#! Specifies a secret to be used when pulling the above `image_repo` container image. #! Can be used when the above image_repo is a private registry. #! Typically the value would be the output of: kubectl create secret docker-registry x --docker-server=https://example.io --docker-username="USERNAME" --docker-password="PASSWORD" --dry-run=client -o json | jq -r '.data[".dockerconfigjson"]' #! Optional. diff --git a/deploy/supervisor/deployment.yaml b/deploy/supervisor/deployment.yaml index 459276fb..d1c25234 100644 --- a/deploy/supervisor/deployment.yaml +++ b/deploy/supervisor/deployment.yaml @@ -2,42 +2,43 @@ #! SPDX-License-Identifier: Apache-2.0 #@ load("@ytt:data", "data") +#@ load("@ytt:json", "json") +#@ load("helpers.lib.yaml", "defaultLabel", "labels", "namespace", "defaultResourceName", "defaultResourceNameWithSuffix") +#@ if not data.values.into_namespace: --- apiVersion: v1 kind: Namespace metadata: name: #@ data.values.namespace - labels: - name: #@ data.values.namespace + labels: #@ labels() +#@ end --- apiVersion: v1 kind: ServiceAccount metadata: - name: #@ data.values.app_name - namespace: #@ data.values.namespace + name: #@ defaultResourceName() + namespace: #@ namespace() + labels: #@ labels() --- apiVersion: v1 kind: ConfigMap metadata: - name: #@ data.values.app_name + "-static-config" - namespace: #@ data.values.namespace - labels: - app: #@ data.values.app_name + name: #@ defaultResourceNameWithSuffix("static-config") + namespace: #@ namespace() + labels: #@ labels() data: #@yaml/text-templated-strings pinniped.yaml: | - names: - dynamicConfigMap: (@= data.values.app_name + "-dynamic-config" @) + labels: (@= json.encode(labels()).rstrip() @) --- #@ if data.values.image_pull_dockerconfigjson and data.values.image_pull_dockerconfigjson != "": apiVersion: v1 kind: Secret metadata: name: image-pull-secret - namespace: #@ data.values.namespace - labels: - app: #@ data.values.app_name + namespace: #@ namespace() + labels: #@ labels() type: kubernetes.io/dockerconfigjson data: .dockerconfigjson: #@ data.values.image_pull_dockerconfigjson @@ -46,27 +47,24 @@ data: apiVersion: apps/v1 kind: Deployment metadata: - name: #@ data.values.app_name - namespace: #@ data.values.namespace - labels: - app: #@ data.values.app_name + name: #@ defaultResourceName() + namespace: #@ namespace() + labels: #@ labels() spec: replicas: #@ data.values.replicas selector: - matchLabels: - app: #@ data.values.app_name + matchLabels: #@ defaultLabel() template: metadata: - labels: - app: #@ data.values.app_name + labels: #@ defaultLabel() spec: - serviceAccountName: #@ data.values.app_name + serviceAccountName: #@ defaultResourceName() #@ if data.values.image_pull_dockerconfigjson and data.values.image_pull_dockerconfigjson != "": imagePullSecrets: - name: image-pull-secret #@ end containers: - - name: pinniped-supervisor + - name: #@ defaultResourceName() #@ if data.values.image_digest: image: #@ data.values.image_repo + "@" + data.values.image_digest #@ else: @@ -89,7 +87,7 @@ spec: volumes: - name: config-volume configMap: - name: #@ data.values.app_name + "-static-config" + name: #@ defaultResourceNameWithSuffix("static-config") - name: podinfo downwardAPI: items: @@ -107,6 +105,5 @@ spec: - weight: 50 podAffinityTerm: labelSelector: - matchLabels: - app: #@ data.values.app_name + matchLabels: #@ defaultLabel() topologyKey: kubernetes.io/hostname diff --git a/deploy/supervisor/helpers.lib.yaml b/deploy/supervisor/helpers.lib.yaml new file mode 100644 index 00000000..f893152e --- /dev/null +++ b/deploy/supervisor/helpers.lib.yaml @@ -0,0 +1,30 @@ +#! Copyright 2020 the Pinniped contributors. All Rights Reserved. +#! SPDX-License-Identifier: Apache-2.0 + +#@ load("@ytt:data", "data") +#@ load("@ytt:template", "template") + +#@ def defaultResourceName(): +#@ return data.values.app_name +#@ end + +#@ def defaultResourceNameWithSuffix(suffix): +#@ return data.values.app_name + "-" + suffix +#@ end + +#@ def namespace(): +#@ if data.values.into_namespace: +#@ return data.values.into_namespace +#@ else: +#@ return data.values.namespace +#@ end +#@ end + +#@ def defaultLabel(): +app: #@ data.values.app_name +#@ end + +#@ def labels(): +_: #@ template.replace(defaultLabel()) +_: #@ template.replace(data.values.custom_labels) +#@ end diff --git a/deploy/supervisor/rbac.yaml b/deploy/supervisor/rbac.yaml index 4066e139..b1ae89cb 100644 --- a/deploy/supervisor/rbac.yaml +++ b/deploy/supervisor/rbac.yaml @@ -2,16 +2,16 @@ #! SPDX-License-Identifier: Apache-2.0 #@ load("@ytt:data", "data") +#@ load("helpers.lib.yaml", "labels", "namespace", "defaultResourceName", "defaultResourceNameWithSuffix") #! Give permission to various objects within the app's own namespace --- apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: - name: #@ data.values.app_name - namespace: #@ data.values.namespace - labels: - app: #@ data.values.app_name + name: #@ defaultResourceName() + namespace: #@ namespace() + labels: #@ labels() rules: - apiGroups: [""] resources: [secrets] @@ -23,15 +23,14 @@ rules: kind: RoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: - name: #@ data.values.app_name - namespace: #@ data.values.namespace - labels: - app: #@ data.values.app_name + name: #@ defaultResourceName() + namespace: #@ namespace() + labels: #@ labels() subjects: - kind: ServiceAccount - name: #@ data.values.app_name - namespace: #@ data.values.namespace + name: #@ defaultResourceName() + namespace: #@ namespace() roleRef: kind: Role - name: #@ data.values.app_name + name: #@ defaultResourceName() apiGroup: rbac.authorization.k8s.io diff --git a/deploy/supervisor/service.yaml b/deploy/supervisor/service.yaml index 6acbe7fc..eb5e0127 100644 --- a/deploy/supervisor/service.yaml +++ b/deploy/supervisor/service.yaml @@ -1,14 +1,17 @@ +#! Copyright 2020 the Pinniped contributors. All Rights Reserved. +#! SPDX-License-Identifier: Apache-2.0 + #@ load("@ytt:data", "data") +#@ load("helpers.lib.yaml", "defaultLabel", "labels", "namespace", "defaultResourceName", "defaultResourceNameWithSuffix") #@ if data.values.service_nodeport_port: --- apiVersion: v1 kind: Service metadata: - name: #@ data.values.app_name + "-nodeport" - namespace: #@ data.values.namespace - labels: - app: #@ data.values.app_name + name: #@ defaultResourceNameWithSuffix("nodeport") + namespace: #@ namespace() + labels: #@ labels() spec: type: NodePort selector: @@ -25,14 +28,12 @@ spec: apiVersion: v1 kind: Service metadata: - name: #@ data.values.app_name + "-clusterip" - namespace: #@ data.values.namespace - labels: - app: #@ data.values.app_name + name: #@ defaultResourceNameWithSuffix("clusterip") + namespace: #@ namespace() + labels: #@ labels() spec: type: ClusterIP - selector: - app: #@ data.values.app_name + selector: #@ defaultLabel() ports: - protocol: TCP port: #@ data.values.service_clusterip_port @@ -44,14 +45,12 @@ spec: apiVersion: v1 kind: Service metadata: - name: #@ data.values.app_name + "-loadbalancer" - namespace: #@ data.values.namespace - labels: - app: #@ data.values.app_name + name: #@ defaultResourceNameWithSuffix("loadbalancer") + namespace: #@ namespace() + labels: #@ labels() spec: type: LoadBalancer - selector: - app: #@ data.values.app_name + selector: #@ defaultLabel() ports: - protocol: TCP port: #@ data.values.service_loadbalancer_port diff --git a/deploy/supervisor/values.yaml b/deploy/supervisor/values.yaml index 6e74ca36..982b96ed 100644 --- a/deploy/supervisor/values.yaml +++ b/deploy/supervisor/values.yaml @@ -5,7 +5,21 @@ --- app_name: pinniped-supervisor + +#! Creates a new namespace statically in yaml with the given name and installs the app into that namespace. namespace: pinniped-supervisor +#! If specified, assumes that a namespace of the given name already exists and installs the app into that namespace. +#! If both `namespace` and `into_namespace` are specified, then only `into_namespace` is used. +into_namespace: #! e.g. my-preexisting-namespace + +#! All resources created statically by yaml at install-time and all resources created dynamically +#! by controllers at runtime will be labelled with `app: $app_name` and also with the labels +#! specified here. The value of `custom_labels` must be a map of string keys to string values. +#! The app can be uninstalled either by: +#! 1. Deleting the static install-time yaml resources including the static namespace, which will cascade and also delete +#! resources that were dynamically created by controllers at runtime +#! 2. Or, deleting all resources by label, which does not assume that there was a static install-time yaml namespace. +custom_labels: {} #! e.g. {myCustomLabelName: myCustomLabelValue, otherCustomLabelName: otherCustomLabelValue} #! Specify how many replicas of the Pinniped server to run. replicas: 2 @@ -15,7 +29,7 @@ image_repo: docker.io/getpinniped/pinniped-server image_digest: #! e.g. sha256:f3c4fdfd3ef865d4b97a1fd295d94acc3f0c654c46b6f27ffad5cf80216903c8 image_tag: latest -#! Specifies a secret to be used when pulling the above container image. +#! Specifies a secret to be used when pulling the above `image_repo` container image. #! Can be used when the above image_repo is a private registry. #! Typically the value would be the output of: kubectl create secret docker-registry x --docker-server=https://example.io --docker-username="USERNAME" --docker-password="PASSWORD" --dry-run=client -o json | jq -r '.data[".dockerconfigjson"]' #! Optional. diff --git a/deploy/supervisor/z0_crd_overlay.yaml b/deploy/supervisor/z0_crd_overlay.yaml new file mode 100644 index 00000000..dc111105 --- /dev/null +++ b/deploy/supervisor/z0_crd_overlay.yaml @@ -0,0 +1,11 @@ +#! Copyright 2020 the Pinniped contributors. All Rights Reserved. +#! SPDX-License-Identifier: Apache-2.0 + +#@ load("@ytt:overlay", "overlay") +#@ load("helpers.lib.yaml", "labels") + +#@overlay/match by=overlay.subset({"kind": "CustomResourceDefinition", "metadata":{"name":"oidcproviderconfigs.config.pinniped.dev"}}), expects=1 +--- +metadata: + #@overlay/match missing_ok=True + labels: #@ labels() diff --git a/hack/lib/tilt/Tiltfile b/hack/lib/tilt/Tiltfile index 4f96c775..0ab02ce7 100644 --- a/hack/lib/tilt/Tiltfile +++ b/hack/lib/tilt/Tiltfile @@ -94,6 +94,7 @@ k8s_yaml(local([ '--data-value', 'image_tag=tilt-dev', '--data-value-yaml', 'replicas=1', '--data-value-yaml', 'service_nodeport_port=31234', + '--data-value-yaml', 'custom_labels={mySupervisorCustomLabelName: mySupervisorCustomLabelValue}', ])) # Tell tilt to watch all of those files for changes. watch_file('../../../deploy/supervisor') @@ -135,7 +136,8 @@ k8s_yaml(local([ '--data-value image_tag=tilt-dev ' + '--data-value kube_cert_agent_image=debian:10.5-slim ' + '--data-value discovery_url=$(TERM=dumb kubectl cluster-info | awk \'/Kubernetes master/ {print $NF}\') ' + - '--data-value-yaml replicas=1', + '--data-value-yaml replicas=1 ' + + '--data-value-yaml "custom_labels={myConciergeCustomLabelName: myConciergeCustomLabelValue}"' ])) # Tell tilt to watch all of those files for changes. watch_file('../../../deploy/concierge') diff --git a/hack/prepare-for-integration-tests.sh b/hack/prepare-for-integration-tests.sh index 5e0296fd..9578efe9 100755 --- a/hack/prepare-for-integration-tests.sh +++ b/hack/prepare-for-integration-tests.sh @@ -212,6 +212,7 @@ kubectl create secret generic "$test_username" \ # supervisor_app_name="pinniped-supervisor" supervisor_namespace="supervisor" +supervisor_custom_labels="{mySupervisorCustomLabelName: mySupervisorCustomLabelValue}" if ! tilt_mode; then pushd deploy/supervisor >/dev/null @@ -222,6 +223,7 @@ if ! tilt_mode; then --data-value "namespace=$supervisor_namespace" \ --data-value "image_repo=$registry_repo" \ --data-value "image_tag=$tag" \ + --data-value-yaml "custom_labels=$supervisor_custom_labels" \ --data-value-yaml 'service_nodeport_port=31234' >"$manifest" kapp deploy --yes --app "$supervisor_app_name" --diff-changes --file "$manifest" @@ -230,21 +232,23 @@ if ! tilt_mode; then fi # -# Deploy Pinniped +# Deploy the Pinniped Concierge # concierge_app_name="pinniped-concierge" concierge_namespace="concierge" webhook_url="https://local-user-authenticator.local-user-authenticator.svc/authenticate" webhook_ca_bundle="$(kubectl get secret local-user-authenticator-tls-serving-certificate --namespace local-user-authenticator -o 'jsonpath={.data.caCertificate}')" discovery_url="$(TERM=dumb kubectl cluster-info | awk '/Kubernetes master/ {print $NF}')" +concierge_custom_labels="{myConciergeCustomLabelName: myConciergeCustomLabelValue}" if ! tilt_mode; then pushd deploy/concierge >/dev/null - log_note "Deploying the Pinniped app to the cluster..." + log_note "Deploying the Pinniped Concierge app to the cluster..." ytt --file . \ --data-value "app_name=$concierge_app_name" \ --data-value "namespace=$concierge_namespace" \ + --data-value-yaml "custom_labels=$concierge_custom_labels" \ --data-value "image_repo=$registry_repo" \ --data-value "image_tag=$tag" \ --data-value "discovery_url=$discovery_url" >"$manifest" @@ -264,6 +268,7 @@ cat </tmp/integration-test-env # The following env vars should be set before running 'go test -v -count 1 ./test/integration' export PINNIPED_TEST_CONCIERGE_NAMESPACE=${concierge_namespace} export PINNIPED_TEST_CONCIERGE_APP_NAME=${concierge_app_name} +export PINNIPED_TEST_CONCIERGE_CUSTOM_LABELS='${concierge_custom_labels}' export PINNIPED_TEST_USER_USERNAME=${test_username} export PINNIPED_TEST_USER_GROUPS=${test_groups} export PINNIPED_TEST_USER_TOKEN=${test_username}:${test_password} @@ -271,6 +276,7 @@ export PINNIPED_TEST_WEBHOOK_ENDPOINT=${webhook_url} export PINNIPED_TEST_WEBHOOK_CA_BUNDLE=${webhook_ca_bundle} export PINNIPED_TEST_SUPERVISOR_NAMESPACE=${supervisor_namespace} export PINNIPED_TEST_SUPERVISOR_APP_NAME=${supervisor_app_name} +export PINNIPED_TEST_SUPERVISOR_CUSTOM_LABELS='${supervisor_custom_labels}' export PINNIPED_TEST_SUPERVISOR_ADDRESS="127.0.0.1:12345" export PINNIPED_TEST_CLI_OIDC_ISSUER=http://127.0.0.1:12346/dex export PINNIPED_TEST_CLI_OIDC_CLIENT_ID=pinniped-cli diff --git a/internal/concierge/server/server.go b/internal/concierge/server/server.go index 23ad1258..203577a9 100644 --- a/internal/concierge/server/server.go +++ b/internal/concierge/server/server.go @@ -17,13 +17,13 @@ import ( loginv1alpha1 "go.pinniped.dev/generated/1.19/apis/login/v1alpha1" "go.pinniped.dev/internal/certauthority/dynamiccertauthority" "go.pinniped.dev/internal/concierge/apiserver" + "go.pinniped.dev/internal/config/concierge" "go.pinniped.dev/internal/controller/identityprovider/idpcache" "go.pinniped.dev/internal/controllermanager" "go.pinniped.dev/internal/downward" "go.pinniped.dev/internal/dynamiccert" "go.pinniped.dev/internal/here" "go.pinniped.dev/internal/registry/credentialrequest" - "go.pinniped.dev/pkg/config" ) // App is an object that represents the pinniped-concierge application. @@ -92,7 +92,7 @@ func addCommandlineFlagsToCommand(cmd *cobra.Command, app *App) { // Boot the aggregated API server, which will in turn boot the controllers. func (a *App) runServer(ctx context.Context) error { // Read the server config file. - cfg, err := config.FromPath(a.configPath) + cfg, err := concierge.FromPath(a.configPath) if err != nil { return fmt.Errorf("could not load config: %w", err) } @@ -124,6 +124,7 @@ func (a *App) runServer(ctx context.Context) error { &controllermanager.Config{ ServerInstallationNamespace: serverInstallationNamespace, NamesConfig: &cfg.NamesConfig, + Labels: cfg.Labels, KubeCertAgentConfig: &cfg.KubeCertAgentConfig, DiscoveryURLOverride: cfg.DiscoveryInfo.URL, DynamicServingCertProvider: dynamicServingCertProvider, diff --git a/pkg/config/config.go b/internal/config/concierge/config.go similarity index 77% rename from pkg/config/config.go rename to internal/config/concierge/config.go index a7f4430c..b942ce78 100644 --- a/pkg/config/config.go +++ b/internal/config/concierge/config.go @@ -1,9 +1,9 @@ // Copyright 2020 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -// Package config contains functionality to load/store api.Config's from/to +// Package concierge contains functionality to load/store Config's from/to // some source. -package config +package concierge import ( "fmt" @@ -13,7 +13,6 @@ import ( "sigs.k8s.io/yaml" "go.pinniped.dev/internal/constable" - "go.pinniped.dev/pkg/config/api" ) const ( @@ -21,20 +20,20 @@ const ( about9Months = 60 * 60 * 24 * 30 * 9 ) -// FromPath loads an api.Config from a provided local file path, inserts any -// defaults (from the api.Config documentation), and verifies that the config is -// valid (per the api.Config documentation). +// FromPath loads an Config from a provided local file path, inserts any +// defaults (from the Config documentation), and verifies that the config is +// valid (per the Config documentation). // -// Note! The api.Config file should contain base64-encoded WebhookCABundle data. +// Note! The Config file should contain base64-encoded WebhookCABundle data. // This function will decode that base64-encoded data to PEM bytes to be stored -// in the api.Config. -func FromPath(path string) (*api.Config, error) { +// in the Config. +func FromPath(path string) (*Config, error) { data, err := ioutil.ReadFile(path) if err != nil { return nil, fmt.Errorf("read file: %w", err) } - var config api.Config + var config Config if err := yaml.Unmarshal(data, &config); err != nil { return nil, fmt.Errorf("decode yaml: %w", err) } @@ -50,10 +49,14 @@ func FromPath(path string) (*api.Config, error) { return nil, fmt.Errorf("validate names: %w", err) } + if config.Labels == nil { + config.Labels = make(map[string]string) + } + return &config, nil } -func maybeSetAPIDefaults(apiConfig *api.APIConfigSpec) { +func maybeSetAPIDefaults(apiConfig *APIConfigSpec) { if apiConfig.ServingCertificateConfig.DurationSeconds == nil { apiConfig.ServingCertificateConfig.DurationSeconds = int64Ptr(aboutAYear) } @@ -63,7 +66,7 @@ func maybeSetAPIDefaults(apiConfig *api.APIConfigSpec) { } } -func maybeSetKubeCertAgentDefaults(cfg *api.KubeCertAgentSpec) { +func maybeSetKubeCertAgentDefaults(cfg *KubeCertAgentSpec) { if cfg.NamePrefix == nil { cfg.NamePrefix = stringPtr("pinniped-kube-cert-agent-") } @@ -73,7 +76,7 @@ func maybeSetKubeCertAgentDefaults(cfg *api.KubeCertAgentSpec) { } } -func validateNames(names *api.NamesConfigSpec) error { +func validateNames(names *NamesConfigSpec) error { missingNames := []string{} if names == nil { missingNames = append(missingNames, "servingCertificateSecret", "credentialIssuerConfig", "apiService") @@ -94,7 +97,7 @@ func validateNames(names *api.NamesConfigSpec) error { return nil } -func validateAPI(apiConfig *api.APIConfigSpec) error { +func validateAPI(apiConfig *APIConfigSpec) error { if *apiConfig.ServingCertificateConfig.DurationSeconds < *apiConfig.ServingCertificateConfig.RenewBeforeSeconds { return constable.Error("durationSeconds cannot be smaller than renewBeforeSeconds") } diff --git a/pkg/config/config_test.go b/internal/config/concierge/config_test.go similarity index 87% rename from pkg/config/config_test.go rename to internal/config/concierge/config_test.go index 8f9dd202..883893c9 100644 --- a/pkg/config/config_test.go +++ b/internal/config/concierge/config_test.go @@ -1,7 +1,7 @@ // Copyright 2020 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -package config +package concierge import ( "io/ioutil" @@ -11,14 +11,13 @@ import ( "github.com/stretchr/testify/require" "go.pinniped.dev/internal/here" - "go.pinniped.dev/pkg/config/api" ) func TestFromPath(t *testing.T) { tests := []struct { name string yaml string - wantConfig *api.Config + wantConfig *Config wantError string }{ { @@ -36,27 +35,34 @@ func TestFromPath(t *testing.T) { credentialIssuerConfig: pinniped-config apiService: pinniped-api kubeCertAgentPrefix: kube-cert-agent-prefix + labels: + myLabelKey1: myLabelValue1 + myLabelKey2: myLabelValue2 KubeCertAgent: namePrefix: kube-cert-agent-name-prefix- image: kube-cert-agent-image imagePullSecrets: [kube-cert-agent-image-pull-secret] `), - wantConfig: &api.Config{ - DiscoveryInfo: api.DiscoveryInfoSpec{ + wantConfig: &Config{ + DiscoveryInfo: DiscoveryInfoSpec{ URL: stringPtr("https://some.discovery/url"), }, - APIConfig: api.APIConfigSpec{ - ServingCertificateConfig: api.ServingCertificateConfigSpec{ + APIConfig: APIConfigSpec{ + ServingCertificateConfig: ServingCertificateConfigSpec{ DurationSeconds: int64Ptr(3600), RenewBeforeSeconds: int64Ptr(2400), }, }, - NamesConfig: api.NamesConfigSpec{ + NamesConfig: NamesConfigSpec{ ServingCertificateSecret: "pinniped-concierge-api-tls-serving-certificate", CredentialIssuerConfig: "pinniped-config", APIService: "pinniped-api", }, - KubeCertAgentConfig: api.KubeCertAgentSpec{ + Labels: map[string]string{ + "myLabelKey1": "myLabelValue1", + "myLabelKey2": "myLabelValue2", + }, + KubeCertAgentConfig: KubeCertAgentSpec{ NamePrefix: stringPtr("kube-cert-agent-name-prefix-"), Image: stringPtr("kube-cert-agent-image"), ImagePullSecrets: []string{"kube-cert-agent-image-pull-secret"}, @@ -72,22 +78,23 @@ func TestFromPath(t *testing.T) { credentialIssuerConfig: pinniped-config apiService: pinniped-api `), - wantConfig: &api.Config{ - DiscoveryInfo: api.DiscoveryInfoSpec{ + wantConfig: &Config{ + DiscoveryInfo: DiscoveryInfoSpec{ URL: nil, }, - APIConfig: api.APIConfigSpec{ - ServingCertificateConfig: api.ServingCertificateConfigSpec{ + APIConfig: APIConfigSpec{ + ServingCertificateConfig: ServingCertificateConfigSpec{ DurationSeconds: int64Ptr(60 * 60 * 24 * 365), // about a year RenewBeforeSeconds: int64Ptr(60 * 60 * 24 * 30 * 9), // about 9 months }, }, - NamesConfig: api.NamesConfigSpec{ + NamesConfig: NamesConfigSpec{ ServingCertificateSecret: "pinniped-concierge-api-tls-serving-certificate", CredentialIssuerConfig: "pinniped-config", APIService: "pinniped-api", }, - KubeCertAgentConfig: api.KubeCertAgentSpec{ + Labels: map[string]string{}, + KubeCertAgentConfig: KubeCertAgentSpec{ NamePrefix: stringPtr("pinniped-kube-cert-agent-"), Image: stringPtr("debian:latest"), }, diff --git a/pkg/config/api/types.go b/internal/config/concierge/types.go similarity index 92% rename from pkg/config/api/types.go rename to internal/config/concierge/types.go index a5052222..392db864 100644 --- a/pkg/config/api/types.go +++ b/internal/config/concierge/types.go @@ -1,19 +1,20 @@ // Copyright 2020 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -package api +package concierge -// Config contains knobs to setup an instance of Pinniped. +// Config contains knobs to setup an instance of the Pinniped Concierge. type Config struct { DiscoveryInfo DiscoveryInfoSpec `json:"discovery"` APIConfig APIConfigSpec `json:"api"` NamesConfig NamesConfigSpec `json:"names"` KubeCertAgentConfig KubeCertAgentSpec `json:"kubeCertAgent"` + Labels map[string]string `json:"labels"` } // DiscoveryInfoSpec contains configuration knobs specific to // pinniped's publishing of discovery information. These values can be -// viewed as overrides, i.e., if these are set, then pinniped will +// viewed as overrides, i.e., if these are set, then Pinniped will // publish these values in its discovery document instead of the ones it finds. type DiscoveryInfoSpec struct { // URL contains the URL at which pinniped can be contacted. @@ -26,7 +27,7 @@ type APIConfigSpec struct { ServingCertificateConfig ServingCertificateConfigSpec `json:"servingCertificate"` } -// NamesConfigSpec configures the names of some Kubernetes resources for Pinniped. +// NamesConfigSpec configures the names of some Kubernetes resources for the Concierge. type NamesConfigSpec struct { ServingCertificateSecret string `json:"servingCertificateSecret"` CredentialIssuerConfig string `json:"credentialIssuerConfig"` diff --git a/internal/config/supervisor/config.go b/internal/config/supervisor/config.go new file mode 100644 index 00000000..eb6836c6 --- /dev/null +++ b/internal/config/supervisor/config.go @@ -0,0 +1,34 @@ +// Copyright 2020 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Package supervisor contains functionality to load/store Config's from/to +// some source. +package supervisor + +import ( + "fmt" + "io/ioutil" + + "sigs.k8s.io/yaml" +) + +// FromPath loads an Config from a provided local file path, inserts any +// defaults (from the Config documentation), and verifies that the config is +// valid (Config documentation). +func FromPath(path string) (*Config, error) { + data, err := ioutil.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("read file: %w", err) + } + + var config Config + if err := yaml.Unmarshal(data, &config); err != nil { + return nil, fmt.Errorf("decode yaml: %w", err) + } + + if config.Labels == nil { + config.Labels = make(map[string]string) + } + + return &config, nil +} diff --git a/internal/config/supervisor/config_test.go b/internal/config/supervisor/config_test.go new file mode 100644 index 00000000..b616fa5b --- /dev/null +++ b/internal/config/supervisor/config_test.go @@ -0,0 +1,69 @@ +// Copyright 2020 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package supervisor + +import ( + "io/ioutil" + "os" + "testing" + + "github.com/stretchr/testify/require" + + "go.pinniped.dev/internal/here" +) + +func TestFromPath(t *testing.T) { + tests := []struct { + name string + yaml string + wantConfig *Config + }{ + { + name: "Happy", + yaml: here.Doc(` + --- + labels: + myLabelKey1: myLabelValue1 + myLabelKey2: myLabelValue2 + `), + wantConfig: &Config{ + Labels: map[string]string{ + "myLabelKey1": "myLabelValue1", + "myLabelKey2": "myLabelValue2", + }, + }, + }, + { + name: "When only the required fields are present, causes other fields to be defaulted", + yaml: here.Doc(` + --- + `), + wantConfig: &Config{ + Labels: map[string]string{}, + }, + }, + } + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + // Write yaml to temp file + f, err := ioutil.TempFile("", "pinniped-test-config-yaml-*") + require.NoError(t, err) + defer func() { + err := os.Remove(f.Name()) + require.NoError(t, err) + }() + _, err = f.WriteString(test.yaml) + require.NoError(t, err) + err = f.Close() + require.NoError(t, err) + + // Test FromPath() + config, err := FromPath(f.Name()) + + require.NoError(t, err) + require.Equal(t, test.wantConfig, config) + }) + } +} diff --git a/internal/config/supervisor/types.go b/internal/config/supervisor/types.go new file mode 100644 index 00000000..03805956 --- /dev/null +++ b/internal/config/supervisor/types.go @@ -0,0 +1,9 @@ +// Copyright 2020 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package supervisor + +// Config contains knobs to setup an instance of the Pinniped Supervisor. +type Config struct { + Labels map[string]string `json:"labels"` +} diff --git a/internal/controller/apicerts/certs_manager.go b/internal/controller/apicerts/certs_manager.go index b8bc8313..541eaf62 100644 --- a/internal/controller/apicerts/certs_manager.go +++ b/internal/controller/apicerts/certs_manager.go @@ -29,6 +29,7 @@ const ( type certsManagerController struct { namespace string certsSecretResourceName string + certsSecretLabels map[string]string k8sClient kubernetes.Interface secretInformer corev1informers.SecretInformer @@ -43,6 +44,7 @@ type certsManagerController struct { func NewCertsManagerController( namespace string, certsSecretResourceName string, + certsSecretLabels map[string]string, k8sClient kubernetes.Interface, secretInformer corev1informers.SecretInformer, withInformer pinnipedcontroller.WithInformerOptionFunc, @@ -57,6 +59,7 @@ func NewCertsManagerController( Syncer: &certsManagerController{ namespace: namespace, certsSecretResourceName: certsSecretResourceName, + certsSecretLabels: certsSecretLabels, k8sClient: k8sClient, secretInformer: secretInformer, certDuration: certDuration, @@ -116,6 +119,7 @@ func (c *certsManagerController) Sync(ctx controllerlib.Context) error { ObjectMeta: metav1.ObjectMeta{ Name: c.certsSecretResourceName, Namespace: c.namespace, + Labels: c.certsSecretLabels, }, StringData: map[string]string{ caCertificateSecretKey: string(aggregatedAPIServerCA.Bundle()), diff --git a/internal/controller/apicerts/certs_manager_test.go b/internal/controller/apicerts/certs_manager_test.go index f7e99bdd..d43babf6 100644 --- a/internal/controller/apicerts/certs_manager_test.go +++ b/internal/controller/apicerts/certs_manager_test.go @@ -42,6 +42,7 @@ func TestManagerControllerOptions(t *testing.T) { _ = NewCertsManagerController( installedInNamespace, certsSecretResourceName, + make(map[string]string), nil, secretsInformer, observableWithInformerOption.WithInformer, @@ -135,6 +136,10 @@ func TestManagerControllerSync(t *testing.T) { subject = NewCertsManagerController( installedInNamespace, certsSecretResourceName, + map[string]string{ + "myLabelKey1": "myLabelValue1", + "myLabelKey2": "myLabelValue2", + }, kubeAPIClient, kubeInformers.Core().V1().Secrets(), controllerlib.WithInformer, @@ -198,6 +203,10 @@ func TestManagerControllerSync(t *testing.T) { actualSecret := actualAction.GetObject().(*corev1.Secret) r.Equal(certsSecretResourceName, actualSecret.Name) r.Equal(installedInNamespace, actualSecret.Namespace) + r.Equal(map[string]string{ + "myLabelKey1": "myLabelValue1", + "myLabelKey2": "myLabelValue2", + }, actualSecret.Labels) actualCACert := actualSecret.StringData["caCertificate"] actualPrivateKey := actualSecret.StringData["tlsPrivateKey"] actualCertChain := actualSecret.StringData["tlsCertificateChain"] diff --git a/internal/controller/issuerconfig/create_or_update_credential_issuer_config.go b/internal/controller/issuerconfig/create_or_update_credential_issuer_config.go index f420b850..a382d7c8 100644 --- a/internal/controller/issuerconfig/create_or_update_credential_issuer_config.go +++ b/internal/controller/issuerconfig/create_or_update_credential_issuer_config.go @@ -21,6 +21,7 @@ func CreateOrUpdateCredentialIssuerConfig( ctx context.Context, credentialIssuerConfigNamespace string, credentialIssuerConfigResourceName string, + credentialIssuerConfigLabels map[string]string, pinnipedClient pinnipedclientset.Interface, applyUpdatesToCredentialIssuerConfigFunc func(configToUpdate *configv1alpha1.CredentialIssuerConfig), ) error { @@ -39,7 +40,9 @@ func CreateOrUpdateCredentialIssuerConfig( if notFound { // Create it - credentialIssuerConfig := minimalValidCredentialIssuerConfig(credentialIssuerConfigResourceName, credentialIssuerConfigNamespace) + credentialIssuerConfig := minimalValidCredentialIssuerConfig( + credentialIssuerConfigResourceName, credentialIssuerConfigNamespace, credentialIssuerConfigLabels, + ) applyUpdatesToCredentialIssuerConfigFunc(credentialIssuerConfig) if _, err := credentialIssuerConfigsClient.Create(ctx, credentialIssuerConfig, metav1.CreateOptions{}); err != nil { @@ -71,12 +74,14 @@ func CreateOrUpdateCredentialIssuerConfig( func minimalValidCredentialIssuerConfig( credentialIssuerConfigName string, credentialIssuerConfigNamespace string, + credentialIssuerConfigLabels map[string]string, ) *configv1alpha1.CredentialIssuerConfig { return &configv1alpha1.CredentialIssuerConfig{ TypeMeta: metav1.TypeMeta{}, ObjectMeta: metav1.ObjectMeta{ Name: credentialIssuerConfigName, Namespace: credentialIssuerConfigNamespace, + Labels: credentialIssuerConfigLabels, }, Status: configv1alpha1.CredentialIssuerConfigStatus{ Strategies: []configv1alpha1.CredentialIssuerConfigStrategy{}, diff --git a/internal/controller/issuerconfig/create_or_update_credential_issuer_config_test.go b/internal/controller/issuerconfig/create_or_update_credential_issuer_config_test.go index 13d1abc2..b20a4756 100644 --- a/internal/controller/issuerconfig/create_or_update_credential_issuer_config_test.go +++ b/internal/controller/issuerconfig/create_or_update_credential_issuer_config_test.go @@ -45,7 +45,15 @@ func TestCreateOrUpdateCredentialIssuerConfig(t *testing.T) { when("the config does not exist", func() { it("creates a new config which includes only the updates made by the func parameter", func() { - err := CreateOrUpdateCredentialIssuerConfig(ctx, installationNamespace, credentialIssuerConfigResourceName, pinnipedAPIClient, + err := CreateOrUpdateCredentialIssuerConfig( + ctx, + installationNamespace, + credentialIssuerConfigResourceName, + map[string]string{ + "myLabelKey1": "myLabelValue1", + "myLabelKey2": "myLabelValue2", + }, + pinnipedAPIClient, func(configToUpdate *configv1alpha1.CredentialIssuerConfig) { configToUpdate.Status.KubeConfigInfo = &configv1alpha1.CredentialIssuerConfigKubeConfigInfo{ CertificateAuthorityData: "some-ca-value", @@ -64,6 +72,10 @@ func TestCreateOrUpdateCredentialIssuerConfig(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: credentialIssuerConfigResourceName, Namespace: installationNamespace, + Labels: map[string]string{ + "myLabelKey1": "myLabelValue1", + "myLabelKey2": "myLabelValue2", + }, }, Status: configv1alpha1.CredentialIssuerConfigStatus{ Strategies: []configv1alpha1.CredentialIssuerConfigStrategy{}, @@ -86,7 +98,12 @@ func TestCreateOrUpdateCredentialIssuerConfig(t *testing.T) { }) it("returns an error", func() { - err := CreateOrUpdateCredentialIssuerConfig(ctx, installationNamespace, credentialIssuerConfigResourceName, pinnipedAPIClient, + err := CreateOrUpdateCredentialIssuerConfig( + ctx, + installationNamespace, + credentialIssuerConfigResourceName, + map[string]string{}, + pinnipedAPIClient, func(configToUpdate *configv1alpha1.CredentialIssuerConfig) {}, ) r.EqualError(err, "could not create or update credentialissuerconfig: create failed: error on create") @@ -103,6 +120,9 @@ func TestCreateOrUpdateCredentialIssuerConfig(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: credentialIssuerConfigResourceName, Namespace: installationNamespace, + Labels: map[string]string{ + "myLabelKey1": "myLabelValue1", + }, }, Status: configv1alpha1.CredentialIssuerConfigStatus{ Strategies: []configv1alpha1.CredentialIssuerConfigStrategy{ @@ -124,7 +144,15 @@ func TestCreateOrUpdateCredentialIssuerConfig(t *testing.T) { }) it("updates the existing config to only apply the updates made by the func parameter", func() { - err := CreateOrUpdateCredentialIssuerConfig(ctx, installationNamespace, credentialIssuerConfigResourceName, pinnipedAPIClient, + err := CreateOrUpdateCredentialIssuerConfig( + ctx, + installationNamespace, + credentialIssuerConfigResourceName, + map[string]string{ + "myLabelKey1": "myLabelValue1", + "myLabelKey2": "myLabelValue2", + }, + pinnipedAPIClient, func(configToUpdate *configv1alpha1.CredentialIssuerConfig) { configToUpdate.Status.KubeConfigInfo.CertificateAuthorityData = "new-ca-value" }, @@ -142,7 +170,12 @@ func TestCreateOrUpdateCredentialIssuerConfig(t *testing.T) { }) it("avoids the cost of an update if the local updates made by the func parameter did not actually change anything", func() { - err := CreateOrUpdateCredentialIssuerConfig(ctx, installationNamespace, credentialIssuerConfigResourceName, pinnipedAPIClient, + err := CreateOrUpdateCredentialIssuerConfig( + ctx, + installationNamespace, + credentialIssuerConfigResourceName, + map[string]string{}, + pinnipedAPIClient, func(configToUpdate *configv1alpha1.CredentialIssuerConfig) { configToUpdate.Status.KubeConfigInfo.CertificateAuthorityData = "initial-ca-value" @@ -166,7 +199,12 @@ func TestCreateOrUpdateCredentialIssuerConfig(t *testing.T) { }) it("returns an error", func() { - err := CreateOrUpdateCredentialIssuerConfig(ctx, installationNamespace, credentialIssuerConfigResourceName, pinnipedAPIClient, + err := CreateOrUpdateCredentialIssuerConfig( + ctx, + installationNamespace, + credentialIssuerConfigResourceName, + map[string]string{}, + pinnipedAPIClient, func(configToUpdate *configv1alpha1.CredentialIssuerConfig) {}, ) r.EqualError(err, "could not create or update credentialissuerconfig: get failed: error on get") @@ -181,7 +219,12 @@ func TestCreateOrUpdateCredentialIssuerConfig(t *testing.T) { }) it("returns an error", func() { - err := CreateOrUpdateCredentialIssuerConfig(ctx, installationNamespace, credentialIssuerConfigResourceName, pinnipedAPIClient, + err := CreateOrUpdateCredentialIssuerConfig( + ctx, + installationNamespace, + credentialIssuerConfigResourceName, + map[string]string{}, + pinnipedAPIClient, func(configToUpdate *configv1alpha1.CredentialIssuerConfig) { configToUpdate.Status.KubeConfigInfo.CertificateAuthorityData = "new-ca-value" }, @@ -215,7 +258,15 @@ func TestCreateOrUpdateCredentialIssuerConfig(t *testing.T) { }) it("retries updates on conflict", func() { - err := CreateOrUpdateCredentialIssuerConfig(ctx, installationNamespace, credentialIssuerConfigResourceName, pinnipedAPIClient, + err := CreateOrUpdateCredentialIssuerConfig( + ctx, + installationNamespace, + credentialIssuerConfigResourceName, + map[string]string{ + "myLabelKey1": "myLabelValue1", + "myLabelKey2": "myLabelValue2", + }, + pinnipedAPIClient, func(configToUpdate *configv1alpha1.CredentialIssuerConfig) { configToUpdate.Status.KubeConfigInfo.CertificateAuthorityData = "new-ca-value" }, diff --git a/internal/controller/issuerconfig/kube_config_info_publisher.go b/internal/controller/issuerconfig/kube_config_info_publisher.go index 3705a3d0..03f2b480 100644 --- a/internal/controller/issuerconfig/kube_config_info_publisher.go +++ b/internal/controller/issuerconfig/kube_config_info_publisher.go @@ -27,6 +27,7 @@ const ( type kubeConigInfoPublisherController struct { credentialIssuerConfigNamespaceName string credentialIssuerConfigResourceName string + credentialIssuerConfigLabels map[string]string serverOverride *string pinnipedClient pinnipedclientset.Interface configMapInformer corev1informers.ConfigMapInformer @@ -38,6 +39,7 @@ type kubeConigInfoPublisherController struct { func NewKubeConfigInfoPublisherController( credentialIssuerConfigNamespaceName string, credentialIssuerConfigResourceName string, + credentialIssuerConfigLabels map[string]string, serverOverride *string, pinnipedClient pinnipedclientset.Interface, configMapInformer corev1informers.ConfigMapInformer, @@ -49,6 +51,7 @@ func NewKubeConfigInfoPublisherController( Syncer: &kubeConigInfoPublisherController{ credentialIssuerConfigResourceName: credentialIssuerConfigResourceName, credentialIssuerConfigNamespaceName: credentialIssuerConfigNamespaceName, + credentialIssuerConfigLabels: credentialIssuerConfigLabels, serverOverride: serverOverride, pinnipedClient: pinnipedClient, configMapInformer: configMapInformer, @@ -114,6 +117,7 @@ func (c *kubeConigInfoPublisherController) Sync(ctx controllerlib.Context) error ctx.Context, c.credentialIssuerConfigNamespaceName, c.credentialIssuerConfigResourceName, + c.credentialIssuerConfigLabels, c.pinnipedClient, updateServerAndCAFunc, ) diff --git a/internal/controller/issuerconfig/kube_config_info_publisher_test.go b/internal/controller/issuerconfig/kube_config_info_publisher_test.go index f3750c1a..73e8afea 100644 --- a/internal/controller/issuerconfig/kube_config_info_publisher_test.go +++ b/internal/controller/issuerconfig/kube_config_info_publisher_test.go @@ -43,6 +43,7 @@ func TestInformerFilters(t *testing.T) { _ = NewKubeConfigInfoPublisherController( installedInNamespace, credentialIssuerConfigResourceName, + map[string]string{}, nil, nil, configMapInformer, @@ -127,6 +128,10 @@ func TestSync(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: credentialIssuerConfigResourceName, Namespace: expectedNamespace, + Labels: map[string]string{ + "myLabelKey1": "myLabelValue1", + "myLabelKey2": "myLabelValue2", + }, }, Status: configv1alpha1.CredentialIssuerConfigStatus{ Strategies: []configv1alpha1.CredentialIssuerConfigStrategy{}, @@ -146,6 +151,10 @@ func TestSync(t *testing.T) { subject = NewKubeConfigInfoPublisherController( installedInNamespace, credentialIssuerConfigResourceName, + map[string]string{ + "myLabelKey1": "myLabelValue1", + "myLabelKey2": "myLabelValue2", + }, serverOverride, pinnipedAPIClient, kubeInformers.Core().V1().ConfigMaps(), diff --git a/internal/controller/kubecertagent/annotater.go b/internal/controller/kubecertagent/annotater.go index 9e6272bb..d7727784 100644 --- a/internal/controller/kubecertagent/annotater.go +++ b/internal/controller/kubecertagent/annotater.go @@ -122,13 +122,7 @@ func (c *annotaterController) Sync(ctx controllerlib.Context) error { keyPath, ); err != nil { err = fmt.Errorf("cannot update agent pod: %w", err) - strategyResultUpdateErr := createOrUpdateCredentialIssuerConfig( - ctx.Context, - *c.credentialIssuerConfigLocationConfig, - c.clock, - c.pinnipedAPIClient, - err, - ) + strategyResultUpdateErr := createOrUpdateCredentialIssuerConfig(ctx.Context, *c.credentialIssuerConfigLocationConfig, nil, c.clock, c.pinnipedAPIClient, err) if strategyResultUpdateErr != nil { // If the CIC update fails, then we probably want to try again. This controller will get // called again because of the pod create failure, so just try the CIC update again then. diff --git a/internal/controller/kubecertagent/creater.go b/internal/controller/kubecertagent/creater.go index 5d3200b3..e284f110 100644 --- a/internal/controller/kubecertagent/creater.go +++ b/internal/controller/kubecertagent/creater.go @@ -23,6 +23,7 @@ import ( type createrController struct { agentPodConfig *AgentPodConfig credentialIssuerConfigLocationConfig *CredentialIssuerConfigLocationConfig + credentialIssuerConfigLabels map[string]string clock clock.Clock k8sClient kubernetes.Interface pinnipedAPIClient pinnipedclientset.Interface @@ -38,6 +39,7 @@ type createrController struct { func NewCreaterController( agentPodConfig *AgentPodConfig, credentialIssuerConfigLocationConfig *CredentialIssuerConfigLocationConfig, + credentialIssuerConfigLabels map[string]string, clock clock.Clock, k8sClient kubernetes.Interface, pinnipedAPIClient pinnipedclientset.Interface, @@ -53,6 +55,7 @@ func NewCreaterController( Syncer: &createrController{ agentPodConfig: agentPodConfig, credentialIssuerConfigLocationConfig: credentialIssuerConfigLocationConfig, + credentialIssuerConfigLabels: credentialIssuerConfigLabels, clock: clock, k8sClient: k8sClient, pinnipedAPIClient: pinnipedAPIClient, @@ -95,6 +98,7 @@ func (c *createrController) Sync(ctx controllerlib.Context) error { return createOrUpdateCredentialIssuerConfig( ctx.Context, *c.credentialIssuerConfigLocationConfig, + c.credentialIssuerConfigLabels, c.clock, c.pinnipedAPIClient, constable.Error("did not find kube-controller-manager pod(s)"), @@ -129,6 +133,7 @@ func (c *createrController) Sync(ctx controllerlib.Context) error { strategyResultUpdateErr := createOrUpdateCredentialIssuerConfig( ctx.Context, *c.credentialIssuerConfigLocationConfig, + c.credentialIssuerConfigLabels, c.clock, c.pinnipedAPIClient, err, diff --git a/internal/controller/kubecertagent/creater_test.go b/internal/controller/kubecertagent/creater_test.go index 58d6c4bd..e6fb80c3 100644 --- a/internal/controller/kubecertagent/creater_test.go +++ b/internal/controller/kubecertagent/creater_test.go @@ -42,7 +42,8 @@ func TestCreaterControllerFilter(t *testing.T) { _ = NewCreaterController( agentPodConfig, credentialIssuerConfigLocationConfig, - nil, // clock, shound't matter + map[string]string{}, + nil, // clock, shouldn't matter nil, // k8sClient, shouldn't matter nil, // pinnipedAPIClient, shouldn't matter kubeSystemPodInformer, @@ -66,7 +67,8 @@ func TestCreaterControllerInitialEvent(t *testing.T) { _ = NewCreaterController( nil, // agentPodConfig, shouldn't matter nil, // credentialIssuerConfigLocationConfig, shouldn't matter - nil, // clock, shound't matter + map[string]string{}, + nil, // clock, shouldn't matter nil, // k8sClient, shouldn't matter nil, // pinnipedAPIClient, shouldn't matter kubeSystemInformers.Core().V1().Pods(), @@ -111,11 +113,19 @@ func TestCreaterControllerSync(t *testing.T) { ContainerImage: "some-agent-image", PodNamePrefix: "some-agent-name-", ContainerImagePullSecrets: []string{"some-image-pull-secret"}, + AdditionalLabels: map[string]string{ + "myLabelKey1": "myLabelValue1", + "myLabelKey2": "myLabelValue2", + }, }, &CredentialIssuerConfigLocationConfig{ Namespace: credentialIssuerConfigNamespaceName, Name: credentialIssuerConfigResourceName, }, + map[string]string{ + "myLabelKey1": "myLabelValue1", + "myLabelKey2": "myLabelValue2", + }, clock.NewFakeClock(frozenNow), kubeAPIClient, pinnipedAPIClient, @@ -361,6 +371,10 @@ func TestCreaterControllerSync(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: credentialIssuerConfigResourceName, Namespace: credentialIssuerConfigNamespaceName, + Labels: map[string]string{ + "myLabelKey1": "myLabelValue1", + "myLabelKey2": "myLabelValue2", + }, }, Status: configv1alpha1.CredentialIssuerConfigStatus{ Strategies: []configv1alpha1.CredentialIssuerConfigStrategy{ @@ -502,6 +516,10 @@ func TestCreaterControllerSync(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: credentialIssuerConfigResourceName, Namespace: credentialIssuerConfigNamespaceName, + Labels: map[string]string{ + "myLabelKey1": "myLabelValue1", + "myLabelKey2": "myLabelValue2", + }, }, Status: configv1alpha1.CredentialIssuerConfigStatus{ Strategies: []configv1alpha1.CredentialIssuerConfigStrategy{ diff --git a/internal/controller/kubecertagent/execer.go b/internal/controller/kubecertagent/execer.go index 6791c6fd..ebc43f78 100644 --- a/internal/controller/kubecertagent/execer.go +++ b/internal/controller/kubecertagent/execer.go @@ -87,39 +87,21 @@ func (c *execerController) Sync(ctx controllerlib.Context) error { certPEM, err := c.podCommandExecutor.Exec(agentPod.Namespace, agentPod.Name, "cat", certPath) if err != nil { - strategyResultUpdateErr := createOrUpdateCredentialIssuerConfig( - ctx.Context, - *c.credentialIssuerConfigLocationConfig, - c.clock, - c.pinnipedAPIClient, - err, - ) + strategyResultUpdateErr := createOrUpdateCredentialIssuerConfig(ctx.Context, *c.credentialIssuerConfigLocationConfig, nil, c.clock, c.pinnipedAPIClient, err) klog.ErrorS(strategyResultUpdateErr, "could not create or update CredentialIssuerConfig with strategy success") return err } keyPEM, err := c.podCommandExecutor.Exec(agentPod.Namespace, agentPod.Name, "cat", keyPath) if err != nil { - strategyResultUpdateErr := createOrUpdateCredentialIssuerConfig( - ctx.Context, - *c.credentialIssuerConfigLocationConfig, - c.clock, - c.pinnipedAPIClient, - err, - ) + strategyResultUpdateErr := createOrUpdateCredentialIssuerConfig(ctx.Context, *c.credentialIssuerConfigLocationConfig, nil, c.clock, c.pinnipedAPIClient, err) klog.ErrorS(strategyResultUpdateErr, "could not create or update CredentialIssuerConfig with strategy success") return err } c.dynamicCertProvider.Set([]byte(certPEM), []byte(keyPEM)) - err = createOrUpdateCredentialIssuerConfig( - ctx.Context, - *c.credentialIssuerConfigLocationConfig, - c.clock, - c.pinnipedAPIClient, - nil, // nil error = success! yay! - ) + err = createOrUpdateCredentialIssuerConfig(ctx.Context, *c.credentialIssuerConfigLocationConfig, nil, c.clock, c.pinnipedAPIClient, nil) if err != nil { return err } diff --git a/internal/controller/kubecertagent/kubecertagent.go b/internal/controller/kubecertagent/kubecertagent.go index 00e5e00e..7f816b39 100644 --- a/internal/controller/kubecertagent/kubecertagent.go +++ b/internal/controller/kubecertagent/kubecertagent.go @@ -61,12 +61,15 @@ type AgentPodConfig struct { // The container image used for the agent pods. ContainerImage string - // The name prefix for each of the agent pods. + // The name prefix for each of the agent pods. PodNamePrefix string // ContainerImagePullSecrets is a list of names of Kubernetes Secret objects that will be used as // ImagePullSecrets on the kube-cert-agent pods. ContainerImagePullSecrets []string + + // Additional labels that should be added to every agent pod during creation. + AdditionalLabels map[string]string } type CredentialIssuerConfigLocationConfig struct { @@ -78,9 +81,13 @@ type CredentialIssuerConfigLocationConfig struct { } func (c *AgentPodConfig) Labels() map[string]string { - return map[string]string{ + labels := map[string]string{ agentPodLabelKey: agentPodLabelValue, } + for k, v := range c.AdditionalLabels { + labels[k] = v + } + return labels } func (c *AgentPodConfig) PodTemplate() *corev1.Pod { @@ -258,9 +265,9 @@ func findControllerManagerPodForSpecificAgentPod( return maybeControllerManagerPod, nil } -func createOrUpdateCredentialIssuerConfig( - ctx context.Context, +func createOrUpdateCredentialIssuerConfig(ctx context.Context, cicConfig CredentialIssuerConfigLocationConfig, + credentialIssuerConfigLabels map[string]string, clock clock.Clock, pinnipedAPIClient pinnipedclientset.Interface, err error, @@ -269,6 +276,7 @@ func createOrUpdateCredentialIssuerConfig( ctx, cicConfig.Namespace, cicConfig.Name, + credentialIssuerConfigLabels, pinnipedAPIClient, func(configToUpdate *configv1alpha1.CredentialIssuerConfig) { var strategyResult configv1alpha1.CredentialIssuerConfigStrategy diff --git a/internal/controller/kubecertagent/kubecertagent_test.go b/internal/controller/kubecertagent/kubecertagent_test.go index 6c8da06a..26011dde 100644 --- a/internal/controller/kubecertagent/kubecertagent_test.go +++ b/internal/controller/kubecertagent/kubecertagent_test.go @@ -79,6 +79,8 @@ func exampleControllerManagerAndAgentPods( Namespace: agentPodNamespace, Labels: map[string]string{ "kube-cert-agent.pinniped.dev": "true", + "myLabelKey1": "myLabelValue1", + "myLabelKey2": "myLabelValue2", }, Annotations: map[string]string{ "kube-cert-agent.pinniped.dev/controller-manager-name": controllerManagerPod.Name, diff --git a/internal/controller/supervisorconfig/jwks.go b/internal/controller/supervisorconfig/jwks.go index a507df90..563df927 100644 --- a/internal/controller/supervisorconfig/jwks.go +++ b/internal/controller/supervisorconfig/jwks.go @@ -56,15 +56,17 @@ func generateECKey(r io.Reader) (interface{}, error) { // jwkController holds the fields necessary for the JWKS controller to communicate with OPC's and // secrets, both via a cache and via the API. type jwksController struct { - pinnipedClient pinnipedclientset.Interface - kubeClient kubernetes.Interface - opcInformer configinformers.OIDCProviderConfigInformer - secretInformer corev1informers.SecretInformer + jwksSecretLabels map[string]string + pinnipedClient pinnipedclientset.Interface + kubeClient kubernetes.Interface + opcInformer configinformers.OIDCProviderConfigInformer + secretInformer corev1informers.SecretInformer } // NewJWKSController returns a controllerlib.Controller that ensures an OPC has a corresponding // Secret that contains a valid active JWK and JWKS. func NewJWKSController( + jwksSecretLabels map[string]string, kubeClient kubernetes.Interface, pinnipedClient pinnipedclientset.Interface, secretInformer corev1informers.SecretInformer, @@ -75,10 +77,11 @@ func NewJWKSController( controllerlib.Config{ Name: "JWKSController", Syncer: &jwksController{ - kubeClient: kubeClient, - pinnipedClient: pinnipedClient, - secretInformer: secretInformer, - opcInformer: opcInformer, + jwksSecretLabels: jwksSecretLabels, + kubeClient: kubeClient, + pinnipedClient: pinnipedClient, + secretInformer: secretInformer, + opcInformer: opcInformer, }, }, // We want to be notified when a OPC's secret gets updated or deleted. When this happens, we @@ -234,6 +237,7 @@ func (c *jwksController) generateSecret(opc *configv1alpha1.OIDCProviderConfig) ObjectMeta: metav1.ObjectMeta{ Name: opc.Name + "-jwks", Namespace: opc.Namespace, + Labels: c.jwksSecretLabels, OwnerReferences: []metav1.OwnerReference{ *metav1.NewControllerRef(opc, schema.GroupVersionKind{ Group: configv1alpha1.SchemeGroupVersion.Group, @@ -241,7 +245,6 @@ func (c *jwksController) generateSecret(opc *configv1alpha1.OIDCProviderConfig) Kind: opcKind, }), }, - // TODO: custom labels. }, Data: map[string][]byte{ activeJWKKey: jwkData, diff --git a/internal/controller/supervisorconfig/jwks_test.go b/internal/controller/supervisorconfig/jwks_test.go index ecab24ad..00e2a15d 100644 --- a/internal/controller/supervisorconfig/jwks_test.go +++ b/internal/controller/supervisorconfig/jwks_test.go @@ -151,6 +151,7 @@ func TestJWKSControllerFilterSecret(t *testing.T) { ).Config().V1alpha1().OIDCProviderConfigs() withInformer := testutil.NewObservableWithInformerOption() _ = NewJWKSController( + nil, // labels, not needed nil, // kubeClient, not needed nil, // pinnipedClient, not needed secretInformer, @@ -204,6 +205,7 @@ func TestJWKSControllerFilterOPC(t *testing.T) { ).Config().V1alpha1().OIDCProviderConfigs() withInformer := testutil.NewObservableWithInformerOption() _ = NewJWKSController( + nil, // labels, not needed nil, // kubeClient, not needed nil, // pinnipedClient, not needed secretInformer, @@ -264,6 +266,10 @@ func TestJWKSControllerSync(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: goodOPCWithStatus.Status.JWKSSecret.Name, Namespace: namespace, + Labels: map[string]string{ + "myLabelKey1": "myLabelValue1", + "myLabelKey2": "myLabelValue2", + }, OwnerReferences: []metav1.OwnerReference{ { APIVersion: opcGVR.GroupVersion().String(), @@ -648,6 +654,10 @@ func TestJWKSControllerSync(t *testing.T) { ) c := NewJWKSController( + map[string]string{ + "myLabelKey1": "myLabelValue1", + "myLabelKey2": "myLabelValue2", + }, kubeAPIClient, pinnipedAPIClient, kubeInformers.Core().V1().Secrets(), diff --git a/internal/controllermanager/prepare_controllers.go b/internal/controllermanager/prepare_controllers.go index 88296f3d..eb83c6a2 100644 --- a/internal/controllermanager/prepare_controllers.go +++ b/internal/controllermanager/prepare_controllers.go @@ -22,6 +22,7 @@ import ( loginv1alpha1 "go.pinniped.dev/generated/1.19/apis/login/v1alpha1" pinnipedclientset "go.pinniped.dev/generated/1.19/client/clientset/versioned" pinnipedinformers "go.pinniped.dev/generated/1.19/client/informers/externalversions" + "go.pinniped.dev/internal/config/concierge" "go.pinniped.dev/internal/controller/apicerts" "go.pinniped.dev/internal/controller/identityprovider/idpcache" "go.pinniped.dev/internal/controller/identityprovider/webhookcachecleaner" @@ -30,7 +31,6 @@ import ( "go.pinniped.dev/internal/controller/kubecertagent" "go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/dynamiccert" - "go.pinniped.dev/pkg/config/api" ) const ( @@ -47,11 +47,11 @@ type Config struct { // NamesConfig comes from the Pinniped config API (see api.Config). It specifies how Kubernetes // objects should be named. - NamesConfig *api.NamesConfigSpec + NamesConfig *concierge.NamesConfigSpec // KubeCertAgentConfig comes from the Pinniped config API (see api.Config). It configures how // the kubecertagent package's controllers should manage the agent pods. - KubeCertAgentConfig *api.KubeCertAgentSpec + KubeCertAgentConfig *concierge.KubeCertAgentSpec // DiscoveryURLOverride allows a caller to inject a hardcoded discovery URL into Pinniped // discovery document. @@ -72,6 +72,9 @@ type Config struct { // IDPCache is a cache of authenticators shared amongst various IDP-related controllers. IDPCache *idpcache.Cache + + // Labels are labels that should be added to any resources created by the controllers. + Labels map[string]string } // Prepare the controllers and their informers and return a function that will start them when called. @@ -96,6 +99,7 @@ func PrepareControllers(c *Config) (func(ctx context.Context), error) { ContainerImage: *c.KubeCertAgentConfig.Image, PodNamePrefix: *c.KubeCertAgentConfig.NamePrefix, ContainerImagePullSecrets: c.KubeCertAgentConfig.ImagePullSecrets, + AdditionalLabels: c.Labels, } credentialIssuerConfigLocationConfig := &kubecertagent.CredentialIssuerConfigLocationConfig{ Namespace: c.ServerInstallationNamespace, @@ -112,6 +116,7 @@ func PrepareControllers(c *Config) (func(ctx context.Context), error) { issuerconfig.NewKubeConfigInfoPublisherController( c.ServerInstallationNamespace, c.NamesConfig.CredentialIssuerConfig, + c.Labels, c.DiscoveryURLOverride, pinnipedClient, informers.kubePublicNamespaceK8s.Core().V1().ConfigMaps(), @@ -125,6 +130,7 @@ func PrepareControllers(c *Config) (func(ctx context.Context), error) { apicerts.NewCertsManagerController( c.ServerInstallationNamespace, c.NamesConfig.ServingCertificateSecret, + c.Labels, k8sClient, informers.installationNamespaceK8s.Core().V1().Secrets(), controllerlib.WithInformer, @@ -174,6 +180,7 @@ func PrepareControllers(c *Config) (func(ctx context.Context), error) { kubecertagent.NewCreaterController( agentPodConfig, credentialIssuerConfigLocationConfig, + c.Labels, clock.RealClock{}, k8sClient, pinnipedClient, diff --git a/test/integration/concierge_api_serving_certs_test.go b/test/integration/concierge_api_serving_certs_test.go index 94f40e12..4ed38c45 100644 --- a/test/integration/concierge_api_serving_certs_test.go +++ b/test/integration/concierge_api_serving_certs_test.go @@ -90,6 +90,10 @@ func TestAPIServingCertificateAutoCreationAndRotation(t *testing.T) { require.NotEmpty(t, initialCACert) require.NotEmpty(t, initialPrivateKey) require.NotEmpty(t, initialCertChain) + for k, v := range env.ConciergeCustomLabels { + require.Equalf(t, v, secret.Labels[k], "expected secret to have label %s: %s", k, v) + } + require.Equal(t, env.ConciergeAppName, secret.Labels["app"]) // Check that the APIService has the same CA. apiService, err := aggregatedClient.ApiregistrationV1().APIServices().Get(ctx, apiServiceName, metav1.GetOptions{}) @@ -115,6 +119,10 @@ func TestAPIServingCertificateAutoCreationAndRotation(t *testing.T) { require.NotEqual(t, initialCACert, regeneratedCACert) require.NotEqual(t, initialPrivateKey, regeneratedPrivateKey) require.NotEqual(t, initialCertChain, regeneratedCertChain) + for k, v := range env.ConciergeCustomLabels { + require.Equalf(t, v, secret.Labels[k], "expected secret to have label `%s: %s`", k, v) + } + require.Equal(t, env.ConciergeAppName, secret.Labels["app"]) // Expect that the APIService was also updated with the new CA. aggregatedAPIUpdated := func() bool { diff --git a/test/integration/concierge_credentialissuerconfig_test.go b/test/integration/concierge_credentialissuerconfig_test.go index 5616b09d..a31f63bd 100644 --- a/test/integration/concierge_credentialissuerconfig_test.go +++ b/test/integration/concierge_credentialissuerconfig_test.go @@ -33,8 +33,14 @@ func TestCredentialIssuerConfig(t *testing.T) { require.Len(t, actualConfigList.Items, 1) + actualConfig := actualConfigList.Items[0] actualStatusKubeConfigInfo := actualConfigList.Items[0].Status.KubeConfigInfo + for k, v := range env.ConciergeCustomLabels { + require.Equalf(t, v, actualConfig.Labels[k], "expected cic to have label `%s: %s`", k, v) + } + require.Equal(t, env.ConciergeAppName, actualConfig.Labels["app"]) + // Verify the cluster strategy status based on what's expected of the test cluster's ability to share signing keys. actualStatusStrategies := actualConfigList.Items[0].Status.Strategies require.Len(t, actualStatusStrategies, 1) diff --git a/test/integration/concierge_kubecertagent_test.go b/test/integration/concierge_kubecertagent_test.go index 6549fa4d..7066dfae 100644 --- a/test/integration/concierge_kubecertagent_test.go +++ b/test/integration/concierge_kubecertagent_test.go @@ -44,6 +44,14 @@ func TestKubeCertAgent(t *testing.T) { require.NotEmpty(t, originalAgentPods.Items) sortPods(originalAgentPods) + for _, agentPod := range originalAgentPods.Items { + // All agent pods should contain all custom labels + for k, v := range env.ConciergeCustomLabels { + require.Equalf(t, v, agentPod.Labels[k], "expected agent pod to have label `%s: %s`", k, v) + } + require.Equal(t, env.ConciergeAppName, agentPod.Labels["app"]) + } + agentPodsReconciled := func() bool { var currentAgentPods *corev1.PodList currentAgentPods, err = kubeClient.CoreV1().Pods(env.ConciergeNamespace).List(ctx, metav1.ListOptions{ diff --git a/test/integration/supervisor_keys_test.go b/test/integration/supervisor_keys_test.go index b6f73e93..52f5ba09 100644 --- a/test/integration/supervisor_keys_test.go +++ b/test/integration/supervisor_keys_test.go @@ -49,6 +49,12 @@ func TestSupervisorOIDCKeys(t *testing.T) { Get(ctx, updatedOPC.Status.JWKSSecret.Name, metav1.GetOptions{}) require.NoError(t, err) + // Ensure that the secret was labelled. + for k, v := range env.SupervisorCustomLabels { + require.Equalf(t, v, secret.Labels[k], "expected secret to have label `%s: %s`", k, v) + } + require.Equal(t, env.SupervisorAppName, secret.Labels["app"]) + // Ensure the secret has an active key. jwkData, ok := secret.Data["activeJWK"] require.True(t, ok, "secret is missing active jwk") diff --git a/test/library/env.go b/test/library/env.go index eba2a0d1..2d1b4a3b 100644 --- a/test/library/env.go +++ b/test/library/env.go @@ -26,13 +26,15 @@ const ( type TestEnv struct { t *testing.T - ConciergeNamespace string `json:"conciergeNamespace"` - SupervisorNamespace string `json:"supervisorNamespace"` - ConciergeAppName string `json:"conciergeAppName"` - SupervisorAppName string `json:"supervisorAppName"` - Capabilities map[Capability]bool `json:"capabilities"` - TestWebhook idpv1alpha1.WebhookIdentityProviderSpec `json:"testWebhook"` - SupervisorAddress string `json:"supervisorAddress"` + ConciergeNamespace string `json:"conciergeNamespace"` + SupervisorNamespace string `json:"supervisorNamespace"` + ConciergeAppName string `json:"conciergeAppName"` + SupervisorAppName string `json:"supervisorAppName"` + SupervisorCustomLabels map[string]string `json:"supervisorCustomLabels"` + ConciergeCustomLabels map[string]string `json:"conciergeCustomLabels"` + Capabilities map[Capability]bool `json:"capabilities"` + TestWebhook idpv1alpha1.WebhookIdentityProviderSpec `json:"testWebhook"` + SupervisorAddress string `json:"supervisorAddress"` TestUser struct { Token string `json:"token"` @@ -89,6 +91,19 @@ func IntegrationEnv(t *testing.T) *TestEnv { result.SupervisorAddress = needEnv("PINNIPED_TEST_SUPERVISOR_ADDRESS") result.TestWebhook.TLS = &idpv1alpha1.TLSSpec{CertificateAuthorityData: needEnv("PINNIPED_TEST_WEBHOOK_CA_BUNDLE")} + conciergeCustomLabelsYAML := needEnv("PINNIPED_TEST_CONCIERGE_CUSTOM_LABELS") + var conciergeCustomLabels map[string]string + err = yaml.Unmarshal([]byte(conciergeCustomLabelsYAML), &conciergeCustomLabels) + require.NoErrorf(t, err, "PINNIPED_TEST_CONCIERGE_CUSTOM_LABELS must be a YAML map of string to string") + result.ConciergeCustomLabels = conciergeCustomLabels + require.NotEmpty(t, result.ConciergeCustomLabels, "PINNIPED_TEST_CONCIERGE_CUSTOM_LABELS cannot be empty") + supervisorCustomLabelsYAML := needEnv("PINNIPED_TEST_SUPERVISOR_CUSTOM_LABELS") + var supervisorCustomLabels map[string]string + err = yaml.Unmarshal([]byte(supervisorCustomLabelsYAML), &supervisorCustomLabels) + require.NoErrorf(t, err, "PINNIPED_TEST_SUPERVISOR_CUSTOM_LABELS must be a YAML map of string to string") + result.SupervisorCustomLabels = supervisorCustomLabels + require.NotEmpty(t, result.SupervisorCustomLabels, "PINNIPED_TEST_SUPERVISOR_CUSTOM_LABELS cannot be empty") + result.OIDCUpstream.Issuer = needEnv("PINNIPED_TEST_CLI_OIDC_ISSUER") result.OIDCUpstream.ClientID = needEnv("PINNIPED_TEST_CLI_OIDC_CLIENT_ID") result.OIDCUpstream.LocalhostPort, _ = strconv.Atoi(needEnv("PINNIPED_TEST_CLI_OIDC_LOCALHOST_PORT"))