From 318fba416596f0bb11c44dc437f1633fbc3d2a67 Mon Sep 17 00:00:00 2001 From: Jun Shun Zhang <42823312+junshun@users.noreply.github.com> Date: Tue, 14 Mar 2023 08:15:07 -0400 Subject: [PATCH] Adding Credential Provider Package with source and helm chart (#829) * Adding Credential Package with source and helm chart * Adding new line to end of Dockerfile and Makefile * Changed matchImages to take array instead of one repository. Change Initialize interface to remove socketpath and add that to bottlerocket constructor, Removed Notes from chart, moved from Deployment to Daemonset * Namespacing serviceaccount.yaml and removing comments from values.yaml * Copy binaries always to cover update case. Update references to docker to Amazon Linux 2 * Adding AWS Profile to be configurable * Update go version to 1.19, cleanup chart, cleanup bottlerocket tests * Move util to pkg/log. Update to go 1.19 in makefile * Removing time from log and updating global reads to group reads * Updating Gosec * Update Makefile for go version * Formatting fixes * Allowed arm builds for linux * Moving constants to individual files. Moved BR socket logic to BR constructor itself. * Removing unused replicas in values.yaml * Adding new lines to partialyaml and updated tests --- .github/workflows/gosec.yml | 2 +- Makefile | 2 +- credentialproviderpackage/Dockerfile | 14 + credentialproviderpackage/Makefile | 41 ++ .../credential-provider-package/.helmignore | 23 ++ .../credential-provider-package/Chart.yaml | 7 + .../templates/_helpers.tpl | 88 +++++ .../templates/daemonset.yaml | 95 +++++ .../templates/serviceaccount.yaml | 13 + .../credential-provider-package/values.yaml | 46 +++ .../cmd/aws-credential-provider/main.go | 128 +++++++ credentialproviderpackage/go.mod | 28 ++ credentialproviderpackage/go.sum | 87 +++++ .../internal/test/files.go | 137 +++++++ .../configurator/bottlerocket/bottlerocket.go | 181 +++++++++ .../bottlerocket/bottlerocket_test.go | 350 ++++++++++++++++++ .../bottlerocket/testdata/testcreds | 3 + .../pkg/configurator/configurator.go | 17 + .../pkg/configurator/linux/linux.go | 226 +++++++++++ .../pkg/configurator/linux/linux_test.go | 228 ++++++++++++ .../templates/credential-provider-config.yaml | 17 + .../expected-config-multiple-patterns.yaml | 18 + .../linux/testdata/expected-config.yaml | 17 + .../pkg/configurator/linux/testdata/testcreds | 3 + .../pkg/constants/constants.go | 12 + .../pkg/filewriter/filewriter.go | 23 ++ .../pkg/filewriter/filewriter_defaults.go | 19 + .../pkg/filewriter/tmp_writer_test.go | 159 ++++++++ .../pkg/filewriter/writer.go | 99 +++++ .../pkg/filewriter/writer_test.go | 203 ++++++++++ credentialproviderpackage/pkg/log/log.go | 18 + .../pkg/templater/partialyaml.go | 29 ++ .../pkg/templater/partialyaml_test.go | 131 +++++++ .../pkg/templater/templater.go | 66 ++++ .../pkg/templater/templater_test.go | 143 +++++++ .../templater/testdata/invalid_template.yaml | 5 + .../pkg/templater/testdata/key4_template.yaml | 6 + .../testdata/partial_yaml_array_expected.yaml | 8 + .../testdata/partial_yaml_map_expected.yaml | 8 + .../partial_yaml_object_expected.yaml | 3 + .../test1_conditional_false_want.yaml | 3 + .../testdata/test1_conditional_true_want.yaml | 5 + .../templater/testdata/test1_template.yaml | 5 + .../testdata/test_indent_template.yaml | 5 + .../templater/testdata/test_indent_want.yaml | 5 + .../pkg/templater/yaml.go | 39 ++ credentialproviderpackage/skaffold.yaml | 24 ++ 47 files changed, 2787 insertions(+), 2 deletions(-) create mode 100644 credentialproviderpackage/Dockerfile create mode 100644 credentialproviderpackage/Makefile create mode 100644 credentialproviderpackage/charts/credential-provider-package/.helmignore create mode 100644 credentialproviderpackage/charts/credential-provider-package/Chart.yaml create mode 100644 credentialproviderpackage/charts/credential-provider-package/templates/_helpers.tpl create mode 100644 credentialproviderpackage/charts/credential-provider-package/templates/daemonset.yaml create mode 100644 credentialproviderpackage/charts/credential-provider-package/templates/serviceaccount.yaml create mode 100644 credentialproviderpackage/charts/credential-provider-package/values.yaml create mode 100644 credentialproviderpackage/cmd/aws-credential-provider/main.go create mode 100644 credentialproviderpackage/go.mod create mode 100644 credentialproviderpackage/go.sum create mode 100644 credentialproviderpackage/internal/test/files.go create mode 100644 credentialproviderpackage/pkg/configurator/bottlerocket/bottlerocket.go create mode 100644 credentialproviderpackage/pkg/configurator/bottlerocket/bottlerocket_test.go create mode 100644 credentialproviderpackage/pkg/configurator/bottlerocket/testdata/testcreds create mode 100644 credentialproviderpackage/pkg/configurator/configurator.go create mode 100644 credentialproviderpackage/pkg/configurator/linux/linux.go create mode 100644 credentialproviderpackage/pkg/configurator/linux/linux_test.go create mode 100644 credentialproviderpackage/pkg/configurator/linux/templates/credential-provider-config.yaml create mode 100644 credentialproviderpackage/pkg/configurator/linux/testdata/expected-config-multiple-patterns.yaml create mode 100644 credentialproviderpackage/pkg/configurator/linux/testdata/expected-config.yaml create mode 100644 credentialproviderpackage/pkg/configurator/linux/testdata/testcreds create mode 100644 credentialproviderpackage/pkg/constants/constants.go create mode 100644 credentialproviderpackage/pkg/filewriter/filewriter.go create mode 100644 credentialproviderpackage/pkg/filewriter/filewriter_defaults.go create mode 100644 credentialproviderpackage/pkg/filewriter/tmp_writer_test.go create mode 100644 credentialproviderpackage/pkg/filewriter/writer.go create mode 100644 credentialproviderpackage/pkg/filewriter/writer_test.go create mode 100644 credentialproviderpackage/pkg/log/log.go create mode 100644 credentialproviderpackage/pkg/templater/partialyaml.go create mode 100644 credentialproviderpackage/pkg/templater/partialyaml_test.go create mode 100644 credentialproviderpackage/pkg/templater/templater.go create mode 100644 credentialproviderpackage/pkg/templater/templater_test.go create mode 100644 credentialproviderpackage/pkg/templater/testdata/invalid_template.yaml create mode 100644 credentialproviderpackage/pkg/templater/testdata/key4_template.yaml create mode 100644 credentialproviderpackage/pkg/templater/testdata/partial_yaml_array_expected.yaml create mode 100644 credentialproviderpackage/pkg/templater/testdata/partial_yaml_map_expected.yaml create mode 100644 credentialproviderpackage/pkg/templater/testdata/partial_yaml_object_expected.yaml create mode 100644 credentialproviderpackage/pkg/templater/testdata/test1_conditional_false_want.yaml create mode 100644 credentialproviderpackage/pkg/templater/testdata/test1_conditional_true_want.yaml create mode 100644 credentialproviderpackage/pkg/templater/testdata/test1_template.yaml create mode 100644 credentialproviderpackage/pkg/templater/testdata/test_indent_template.yaml create mode 100644 credentialproviderpackage/pkg/templater/testdata/test_indent_want.yaml create mode 100644 credentialproviderpackage/pkg/templater/yaml.go create mode 100644 credentialproviderpackage/skaffold.yaml diff --git a/.github/workflows/gosec.yml b/.github/workflows/gosec.yml index 154758c1..0f620692 100644 --- a/.github/workflows/gosec.yml +++ b/.github/workflows/gosec.yml @@ -17,4 +17,4 @@ jobs: - name: Run Gosec Security Scanner uses: securego/gosec@master with: - args: --exclude-dir=kubetest-plugins --exclude-dir generatebundlefile --exclude-dir ecrtokenrefresher ./... + args: --exclude-dir=kubetest-plugins --exclude-dir generatebundlefile --exclude-dir ecrtokenrefresher --exclude-dir credentialproviderpackage ./... diff --git a/Makefile b/Makefile index b88dbb0a..bed4f9eb 100644 --- a/Makefile +++ b/Makefile @@ -82,7 +82,7 @@ vet: ## Run go vet against code. gosec: ## Run gosec against code. $(GO) install github.com/securego/gosec/v2/cmd/gosec@latest - gosec --exclude-dir generatebundlefile --exclude-dir ecrtokenrefresher ./... + gosec --exclude-dir generatebundlefile --exclude-dir ecrtokenrefresher --exclude-dir credentialproviderpackage ./... SIGNED_ARTIFACTS = pkg/signature/testdata/packagebundle_minControllerVersion.yaml.signed pkg/signature/testdata/packagebundle_valid.yaml.signed pkg/signature/testdata/pod_valid.yaml.signed api/testdata/bundle_one.yaml.signed api/testdata/bundle_two.yaml.signed ENVTEST_ASSETS_DIR=$(shell pwd)/testbin diff --git a/credentialproviderpackage/Dockerfile b/credentialproviderpackage/Dockerfile new file mode 100644 index 00000000..6cadc0b1 --- /dev/null +++ b/credentialproviderpackage/Dockerfile @@ -0,0 +1,14 @@ +FROM golang:1.19-buster +ENV GOTRACEBACK=single +ENV GOPROXY=direct +WORKDIR /app +COPY go.mod . +COPY go.sum . +COPY cmd/ cmd/ +COPY ecr-credential-provider /eksa-binaries/ +COPY aws_signing_helper /eksa-binaries/ +COPY pkg/ pkg/ +ARG SKAFFOLD_GO_GCFLAGS +RUN go build -gcflags="${SKAFFOLD_GO_GCFLAGS}" -o app cmd/aws-credential-provider/*.go + +CMD ["/app/app"] diff --git a/credentialproviderpackage/Makefile b/credentialproviderpackage/Makefile new file mode 100644 index 00000000..9475500a --- /dev/null +++ b/credentialproviderpackage/Makefile @@ -0,0 +1,41 @@ +SHELL = /usr/bin/env bash -o pipefail +.SHELLFLAGS = -ec + +REPO_ROOT=$(shell git rev-parse --show-toplevel) +PROJECT_ROOT=$(REPO_ROOT)/credentialproviderpackage +GOLANG_VERSION?="1.19" +GO ?= $(shell source $(REPO_ROOT)/scripts/common.sh && build::common::get_go_path $(GOLANG_VERSION))/go + +# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) +ifeq (,$(shell go env GOBIN)) +GOBIN=$(shell go env GOPATH)/bin +else +GOBIN=$(shell go env GOBIN) +endif + +all: build + +help: ## Display this help. + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) + +clean: ## Clean output directory, and the built binary + rm -rf output/ + rm -rf bin/* + rm cover.out + +##@ Build + +build: ## Build Binary + mkdir -p $(PROJECT_ROOT)/bin + $(GO) mod tidy -compat=$(GOLANG_VERSION) + $(GO) build -o $(PROJECT_ROOT)/bin/aws-credential-provider $(PROJECT_ROOT)/cmd/aws-credential-provider/*.go + +build-linux: + [ -d bin ] || mkdir bin + env CGO_ENABLED=0 GOOS=linux $(MAKE) build + +run: + $(GO) run . + +test: build + $(GO) test ./... `$(GO) list $(GOTESTS) | grep -v mocks | grep -v fake | grep -v testutil` -coverprofile cover.out diff --git a/credentialproviderpackage/charts/credential-provider-package/.helmignore b/credentialproviderpackage/charts/credential-provider-package/.helmignore new file mode 100644 index 00000000..0e8a0eb3 --- /dev/null +++ b/credentialproviderpackage/charts/credential-provider-package/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/credentialproviderpackage/charts/credential-provider-package/Chart.yaml b/credentialproviderpackage/charts/credential-provider-package/Chart.yaml new file mode 100644 index 00000000..45fe512c --- /dev/null +++ b/credentialproviderpackage/charts/credential-provider-package/Chart.yaml @@ -0,0 +1,7 @@ +apiVersion: v2 +name: credential-provider-package +description: A Helm chart for credential-provider-package, an application for configuring credentials via Kubelet Credential Provider +type: application +version: 0.1.0 +sources: + - https://github.com/aws/eks-anywhere-packages/credentialproviderpackage diff --git a/credentialproviderpackage/charts/credential-provider-package/templates/_helpers.tpl b/credentialproviderpackage/charts/credential-provider-package/templates/_helpers.tpl new file mode 100644 index 00000000..629f9696 --- /dev/null +++ b/credentialproviderpackage/charts/credential-provider-package/templates/_helpers.tpl @@ -0,0 +1,88 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "credential-provider.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "credential-provider.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "credential-provider.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "credential-provider.labels" -}} +helm.sh/chart: {{ include "credential-provider.chart" . }} +{{ include "credential-provider.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "credential-provider.selectorLabels" -}} +app.kubernetes.io/name: {{ include "credential-provider.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "credential-provider.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "credential-provider.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} + +{{/* +Create image name +*/}} +{{- define "template.image" -}} +{{- if eq (substr 0 7 .tag) "sha256:" -}} +{{- printf "/%s@%s" .repository .tag -}} +{{- else -}} +{{- printf "/%s:%s" .repository .tag -}} +{{- end -}} +{{- end -}} + +{{/* +Function to figure out os name +*/}} +{{- define "template.getOSName" -}} +{{- with first ((lookup "v1" "Node" "" "").items) -}} +{{- if contains "Bottlerocket" .status.nodeInfo.osImage -}} +{{- printf "bottlerocket" -}} +{{- else if contains "Amazon Linux" .status.nodeInfo.osImage -}} +{{- printf "amazonlinux" -}} +{{- else -}} +{{- printf "other" -}} +{{- end }} +{{- end }} +{{- end }} diff --git a/credentialproviderpackage/charts/credential-provider-package/templates/daemonset.yaml b/credentialproviderpackage/charts/credential-provider-package/templates/daemonset.yaml new file mode 100644 index 00000000..d4d49420 --- /dev/null +++ b/credentialproviderpackage/charts/credential-provider-package/templates/daemonset.yaml @@ -0,0 +1,95 @@ +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: {{ include "credential-provider.fullname" . }} + namespace: {{ .Release.Namespace | default .Values.defaultNamespace | quote }} + labels: + {{- include "credential-provider.labels" . | nindent 4 }} +spec: + selector: + matchLabels: + {{- include "credential-provider.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "credential-provider.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "credential-provider.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: credential-provider + image: {{ .Values.sourceRegistry }}/{{ .Values.image.repository }}@{{ .Values.image.digest }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + securityContext: + privileged: true + resources: + {{- toYaml .Values.resources | nindent 12 }} + volumeMounts: + - name: aws-creds + mountPath: /secrets/aws-creds + {{ $os := include "template.getOSName" .}} + {{- if eq $os "bottlerocket" }} + - mountPath: /run/api.sock + name: socket + {{- else}} + - mountPath: /node-files/kubelet-extra-args + name: kubelet-extra-args + - name: package-mounts + mountPath: /eksa-packages + {{- end}} + env: + - name: OS_TYPE + value: {{ $os }} + - name: AWS_PROFILE + value: {{.Values.application.profile}} + - name: MATCH_IMAGES + value: '{{ join "," .Values.application.matchImages }}' + - name: DEFAULT_CACHE_DURATION + value: {{.Values.application.defaultCacheDuration}} + volumes: + - name: aws-creds + secret: + secretName: {{.Values.application.secretName}} + optional: false + {{- if eq $os "bottlerocket" }} + - name: socket + hostPath: + path: /run/api.sock + {{- else if eq $os "amazonlinux"}} + - name: kubelet-extra-args + hostPath: + path: /etc/default/kubelet + type: FileOrCreate + {{- else}} + - name: kubelet-extra-args + hostPath: + path: /etc/sysconfig/kubelet + type: FileOrCreate + {{- end }} + {{- if ne $os "bottlerocket" }} + - name: package-mounts + hostPath: + path: /eksa-packages + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + hostPID: true diff --git a/credentialproviderpackage/charts/credential-provider-package/templates/serviceaccount.yaml b/credentialproviderpackage/charts/credential-provider-package/templates/serviceaccount.yaml new file mode 100644 index 00000000..9d261be5 --- /dev/null +++ b/credentialproviderpackage/charts/credential-provider-package/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "credential-provider.serviceAccountName" . }} + namespace: {{ .Release.Namespace | default .Values.defaultNamespace | quote }} + labels: + {{- include "credential-provider.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/credentialproviderpackage/charts/credential-provider-package/values.yaml b/credentialproviderpackage/charts/credential-provider-package/values.yaml new file mode 100644 index 00000000..24745371 --- /dev/null +++ b/credentialproviderpackage/charts/credential-provider-package/values.yaml @@ -0,0 +1,46 @@ +# Default values for credential-provider. +# This is a YAML-formatted file. + +# -- sourceRegistry for all container images in chart. +sourceRegistry: public.ecr.aws/eks-anywhere +defaultNamespace: eksa-packages + +image: + repository: "credential-provider-package" + tag: "{{credential-provider-package-tag}}" + digest: "{{credential-provider-package-digest}}" + pullPolicy: IfNotPresent + +# application values +application: + secretName: aws-cred + matchImages: [] + defaultCacheDuration: "" + profile: "" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} + +podSecurityContext: {} + +securityContext: {} + +resources: {} + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/credentialproviderpackage/cmd/aws-credential-provider/main.go b/credentialproviderpackage/cmd/aws-credential-provider/main.go new file mode 100644 index 00000000..b1621f8a --- /dev/null +++ b/credentialproviderpackage/cmd/aws-credential-provider/main.go @@ -0,0 +1,128 @@ +package main + +import ( + _ "embed" + "os" + "strings" + + "github.com/fsnotify/fsnotify" + + cfg "credential-provider/pkg/configurator" + "credential-provider/pkg/configurator/bottlerocket" + "credential-provider/pkg/configurator/linux" + "credential-provider/pkg/constants" + "credential-provider/pkg/log" +) + +const ( + bottleRocket = "bottlerocket" + socketPath = "/run/api.sock" + + // Aws Credentials + credSrcPath = "/secrets/aws-creds/config" + awsProfile = "eksa-packages" + credWatchData = "/secrets/aws-creds/..data" + credWatchPath = "/secrets/aws-creds/" +) + +func main() { + var configurator cfg.Configurator + var err error + osType := strings.ToLower(os.Getenv("OS_TYPE")) + if osType == "" { + log.ErrorLogger.Println("Missing Environment Variable OS_TYPE") + os.Exit(1) + } + profile := os.Getenv("AWS_PROFILE") + if profile == "" { + profile = awsProfile + } + config := createCredentialProviderConfigOptions() + if osType == bottleRocket { + configurator, err = bottlerocket.NewBottleRocketConfigurator(socketPath) + if err != nil { + log.ErrorLogger.Fatal(err) + } + } else { + configurator = linux.NewLinuxConfigurator() + } + + configurator.Initialize(config) + err = configurator.UpdateAWSCredentials(credSrcPath, profile) + if err != nil { + log.ErrorLogger.Fatal(err) + } + log.InfoLogger.Println("Aws credentials configured") + + err = configurator.UpdateCredentialProvider(profile) + if err != nil { + log.ErrorLogger.Fatal(err) + } + log.InfoLogger.Println("Credential Provider Configured") + + err = configurator.CommitChanges() + if err != nil { + log.ErrorLogger.Fatal(err) + } + + log.InfoLogger.Println("Kubelet Restarted") + + // Creating watcher for credentials + watcher, err := fsnotify.NewWatcher() + if err != nil { + log.ErrorLogger.Fatal(err) + } + defer watcher.Close() + + // Start listening for changes to the aws credentials + go func() { + for { + select { + case event, ok := <-watcher.Events: + if !ok { + return + } + if event.Has(fsnotify.Create) { + if event.Name == credWatchData { + err = configurator.UpdateAWSCredentials(credSrcPath, profile) + if err != nil { + log.ErrorLogger.Fatal(err) + } + log.InfoLogger.Println("Aws credentials successfully changed") + } + } + case err, ok := <-watcher.Errors: + if !ok { + return + } + log.WarningLogger.Printf("filewatcher error: %v", err) + } + } + }() + + err = watcher.Add(credWatchPath) + if err != nil { + log.ErrorLogger.Fatal(err) + } + + // Block main goroutine forever. + <-make(chan struct{}) +} + +func createCredentialProviderConfigOptions() constants.CredentialProviderConfigOptions { + imagePatternsValues := os.Getenv("MATCH_IMAGES") + if imagePatternsValues == "" { + imagePatternsValues = constants.DefaultImagePattern + } + imagePatterns := strings.Split(imagePatternsValues, ",") + + defaultCacheDuration := os.Getenv("DEFAULT_CACHE_DURATION") + if defaultCacheDuration == "" { + defaultCacheDuration = constants.DefaultCacheDuration + } + + return constants.CredentialProviderConfigOptions{ + ImagePatterns: imagePatterns, + DefaultCacheDuration: defaultCacheDuration, + } +} diff --git a/credentialproviderpackage/go.mod b/credentialproviderpackage/go.mod new file mode 100644 index 00000000..069ffee7 --- /dev/null +++ b/credentialproviderpackage/go.mod @@ -0,0 +1,28 @@ +module credential-provider + +go 1.19 + +require ( + github.com/fsnotify/fsnotify v1.6.0 + github.com/google/go-cmp v0.5.9 + github.com/mitchellh/go-ps v1.0.0 + github.com/stretchr/testify v1.8.0 + k8s.io/apimachinery v0.26.0 + sigs.k8s.io/yaml v1.3.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/go-logr/logr v1.2.3 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/sys v0.4.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/klog/v2 v2.80.1 // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect +) diff --git a/credentialproviderpackage/go.sum b/credentialproviderpackage/go.sum new file mode 100644 index 00000000..82b56f68 --- /dev/null +++ b/credentialproviderpackage/go.sum @@ -0,0 +1,87 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= +github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= +github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.3.1-0.20221206200815-1e63c2f08a10 h1:Frnccbp+ok2GkUS2tC84yAq/U9Vg+0sIO7aRL3T4Xnc= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= +golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/apimachinery v0.26.0 h1:1feANjElT7MvPqp0JT6F3Ss6TWDwmcjLypwoPpEf7zg= +k8s.io/apimachinery v0.26.0/go.mod h1:tnPmbONNJ7ByJNz9+n9kMjNP8ON+1qoAIIC70lztu74= +k8s.io/klog/v2 v2.80.1 h1:atnLQ121W371wYYFawwYx1aEY2eUfs4l3J72wtgAwV4= +k8s.io/klog/v2 v2.80.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/utils v0.0.0-20221107191617-1a15be271d1d h1:0Smp/HP1OH4Rvhe+4B8nWGERtlqAGSftbSbbmm45oFs= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= +sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= +sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/credentialproviderpackage/internal/test/files.go b/credentialproviderpackage/internal/test/files.go new file mode 100644 index 00000000..d654aecc --- /dev/null +++ b/credentialproviderpackage/internal/test/files.go @@ -0,0 +1,137 @@ +package test + +import ( + "bytes" + "flag" + "fmt" + "io/ioutil" + "log" + "os" + "os/exec" + "regexp" + "testing" + + "credential-provider/pkg/filewriter" +) + +var UpdateGoldenFiles = flag.Bool("update", false, "update golden files") + +func AssertFilesEquals(t *testing.T, gotPath, wantPath string) { + t.Helper() + gotFile := ReadFile(t, gotPath) + processUpdate(t, wantPath, gotFile) + wantFile := ReadFile(t, wantPath) + + if gotFile != wantFile { + cmd := exec.Command("diff", wantPath, gotPath) + result, err := cmd.Output() + if err != nil { + if exitError, ok := err.(*exec.ExitError); ok { + if exitError.ExitCode() == 1 { + t.Fatalf("Results diff expected actual:\n%s", string(result)) + } + } + } + t.Fatalf("Files are different got =\n %s \n want =\n %s\n%s", gotFile, wantFile, err) + } +} + +func AssertContentToFile(t *testing.T, gotContent, wantFile string) { + t.Helper() + if wantFile == "" { + return + } + processUpdate(t, wantFile, gotContent) + + fileContent := ReadFile(t, wantFile) + if gotContent != fileContent { + diff, err := computeDiffBetweenContentAndFile([]byte(gotContent), wantFile) + if err != nil { + t.Fatalf("Content doesn't match file got =\n%s\n\n\nwant =\n%s\n", gotContent, fileContent) + } + if diff != "" { + t.Fatalf("Results diff expected actual for %s:\n%s", wantFile, string(diff)) + } + } +} + +func contentEqualToFile(gotContent []byte, wantFile string) (bool, error) { + if wantFile == "" && len(gotContent) == 0 { + return false, nil + } + + fileContent, err := ioutil.ReadFile(wantFile) + if err != nil { + return false, err + } + + return bytes.Equal(gotContent, fileContent), nil +} + +func computeDiffBetweenContentAndFile(content []byte, file string) (string, error) { + cmd := exec.Command("diff", "-u", file, "-") + cmd.Stdin = bytes.NewReader([]byte(content)) + result, err := cmd.Output() + if err != nil { + if exitError, ok := err.(*exec.ExitError); ok && exitError.ExitCode() == 1 { + return string(result), nil + } + + return "", fmt.Errorf("computing the difference between content and file %s: %v", file, err) + } + return "", nil +} + +func processUpdate(t *testing.T, filePath, content string) { + if *UpdateGoldenFiles { + if err := ioutil.WriteFile(filePath, []byte(content), 0o644); err != nil { + t.Fatalf("failed to update golden file %s: %v", filePath, err) + } + log.Printf("Golden file updated: %s", filePath) + } +} + +func ReadFileAsBytes(t *testing.T, file string) []byte { + bytesRead, err := ioutil.ReadFile(file) + if err != nil { + t.Fatalf("File [%s] reading error in test: %v", file, err) + } + + return bytesRead +} + +func ReadFile(t *testing.T, file string) string { + return string(ReadFileAsBytes(t, file)) +} + +func NewWriter(t *testing.T) (dir string, writer filewriter.FileWriter) { + dir, err := ioutil.TempDir(".", SanitizePath(t.Name())+"-") + if err != nil { + t.Fatalf("error setting up folder for test: %v", err) + } + + t.Cleanup(cleanupDir(t, dir)) + writer, err = filewriter.NewWriter(dir) + if err != nil { + t.Fatalf("error creating writer with folder for test: %v", err) + } + return dir, writer +} + +func cleanupDir(t *testing.T, dir string) func() { + return func() { + if !t.Failed() { + os.RemoveAll(dir) + } + } +} + +var sanitizePathChars = regexp.MustCompile(`[^\w-]`) + +const sanitizePathReplacementChar = "_" + +// SanitizePath sanitizes s so its usable as a path name. For safety, it assumes all characters that are not +// A-Z, a-z, 0-9, _ or - are illegal and replaces them with _. +func SanitizePath(s string) string { + return sanitizePathChars.ReplaceAllString(s, sanitizePathReplacementChar) +} diff --git a/credentialproviderpackage/pkg/configurator/bottlerocket/bottlerocket.go b/credentialproviderpackage/pkg/configurator/bottlerocket/bottlerocket.go new file mode 100644 index 00000000..93adb9e4 --- /dev/null +++ b/credentialproviderpackage/pkg/configurator/bottlerocket/bottlerocket.go @@ -0,0 +1,181 @@ +package bottlerocket + +import ( + "bytes" + "context" + "encoding/base64" + "encoding/json" + "fmt" + "io/fs" + "io/ioutil" + "net" + "net/http" + "os" + + "credential-provider/pkg/configurator" + "credential-provider/pkg/constants" +) + +type bottleRocket struct { + client http.Client + baseURL string + config constants.CredentialProviderConfigOptions +} + +type awsCred struct { + Aws aws `json:"aws"` +} +type aws struct { + Config string `json:"config"` + Profile string `json:"profile"` + Region string `json:"region"` +} + +type brKubernetes struct { + Kubernetes kubernetes `json:"kubernetes"` +} +type ecrCredentialProvider struct { + CacheDuration string `json:"cache-duration"` + Enabled bool `json:"enabled"` + ImagePatterns []string `json:"image-patterns"` +} +type credentialProviders struct { + EcrCredentialProvider ecrCredentialProvider `json:"ecr-credential-provider"` +} +type kubernetes struct { + CredentialProviders credentialProviders `json:"credential-providers"` +} + +var _ configurator.Configurator = (*bottleRocket)(nil) + +func NewBottleRocketConfigurator(socketPath string) (*bottleRocket, error) { + socket, err := os.Stat(socketPath) + if err != nil { + return nil, err + } + if socket.Mode().Type() != fs.ModeSocket { + return nil, fmt.Errorf("Unexpected type %s expected socket\n", socket.Mode().Type()) + } + return &bottleRocket{ + client: http.Client{ + Transport: &http.Transport{ + DialContext: func(_ context.Context, _, _ string) (net.Conn, error) { + return net.Dial("unix", socketPath) + }, + }, + }, + }, nil +} + +func (b *bottleRocket) Initialize(config constants.CredentialProviderConfigOptions) { + b.baseURL = "http://localhost/" + b.config = config +} + +func (b *bottleRocket) UpdateAWSCredentials(path string, profile string) error { + data, err := ioutil.ReadFile(path) + if err != nil { + return err + } + + content := base64.StdEncoding.EncodeToString(data) + payload, err := createCredentialsPayload(content, profile) + if err != nil { + return err + } + err = b.sendSettingsSetRequest(payload) + if err != nil { + return err + } + + err = b.CommitChanges() + if err != nil { + return err + } + + return err +} + +func (b *bottleRocket) UpdateCredentialProvider(_ string) error { + payload, err := createCredentialProviderPayload(b.config) + if err != nil { + return err + } + err = b.sendSettingsSetRequest(payload) + if err != nil { + return err + } + + return err +} + +func (b *bottleRocket) CommitChanges() error { + // For Bottlerocket this step is committing all changes at once + commitPath := b.baseURL + "tx/commit_and_apply" + resp, err := b.client.Post(commitPath, "application/json", bytes.NewBuffer(make([]byte, 0))) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("failed to commit changes: %s", resp.Status) + } + return nil +} + +func (b *bottleRocket) sendSettingsSetRequest(payload []byte) error { + settingsPath := b.baseURL + "settings" + req, err := http.NewRequest(http.MethodPatch, settingsPath, bytes.NewBuffer(payload)) + if err != nil { + return err + } + + req.Header.Set("Content-Type", "application/json") + respPatch, err := b.client.Do(req) + if err != nil { + return err + } + defer respPatch.Body.Close() + + if respPatch.StatusCode != http.StatusNoContent { + return fmt.Errorf("failed patch request: %s", respPatch.Status) + } + + return nil + +} + +func createCredentialsPayload(content string, profile string) ([]byte, error) { + aws := aws{ + Config: content, + Profile: profile, + } + + creds := awsCred{Aws: aws} + + payload, err := json.Marshal(creds) + if err != nil { + return nil, err + } + return payload, nil +} + +func createCredentialProviderPayload(config constants.CredentialProviderConfigOptions) ([]byte, error) { + providerConfig := brKubernetes{ + Kubernetes: kubernetes{ + credentialProviders{ + ecrCredentialProvider{ + Enabled: true, + ImagePatterns: config.ImagePatterns, + CacheDuration: config.DefaultCacheDuration, + }, + }, + }, + } + + payload, err := json.Marshal(providerConfig) + if err != nil { + return nil, err + } + return payload, nil +} diff --git a/credentialproviderpackage/pkg/configurator/bottlerocket/bottlerocket_test.go b/credentialproviderpackage/pkg/configurator/bottlerocket/bottlerocket_test.go new file mode 100644 index 00000000..6490a06e --- /dev/null +++ b/credentialproviderpackage/pkg/configurator/bottlerocket/bottlerocket_test.go @@ -0,0 +1,350 @@ +package bottlerocket + +import ( + "bytes" + "encoding/base64" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/http/httptest" + "os" + "testing" + + "github.com/stretchr/testify/assert" + + "credential-provider/pkg/constants" +) + +type response struct { + statusCode int + expectedBody []byte + responseMsg string +} + +func Test_bottleRocket_CommitChanges(t *testing.T) { + type fields struct { + client http.Client + baseURL string + config constants.CredentialProviderConfigOptions + } + + tests := []struct { + name string + fields fields + wantErr bool + response response + expected string + }{ + { + name: "test success", + fields: fields{ + client: http.Client{}, + config: constants.CredentialProviderConfigOptions{ + ImagePatterns: []string{constants.DefaultImagePattern}, + DefaultCacheDuration: constants.DefaultCacheDuration, + }, + }, + wantErr: false, + response: response{ + statusCode: http.StatusOK, + responseMsg: "", + }, + }, + { + name: "test fail", + fields: fields{ + client: http.Client{}, + config: constants.CredentialProviderConfigOptions{ + ImagePatterns: []string{constants.DefaultImagePattern}, + DefaultCacheDuration: constants.DefaultCacheDuration, + }, + }, + wantErr: true, + response: response{ + statusCode: http.StatusNotFound, + responseMsg: "", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(tt.response.statusCode) + fmt.Fprintf(w, tt.response.responseMsg) + })) + b := &bottleRocket{ + client: tt.fields.client, + baseURL: svr.URL + "/", + config: tt.fields.config, + } + if err := b.CommitChanges(); (err != nil) != tt.wantErr { + t.Errorf("UpdateAWSCredentials() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func Test_bottleRocket_UpdateAWSCredentials(t *testing.T) { + file, err := os.Open("testdata/testcreds") + if err != nil { + t.Errorf("Failed to open testcreds") + } + content, err := io.ReadAll(file) + if err != nil { + t.Errorf("Failed to read testcreds") + } + encodedSecret := base64.StdEncoding.EncodeToString(content) + expectedBody := fmt.Sprintf("{\"aws\":{\"config\":\"%s\",\"profile\":\"eksa-packages\",\"region\":\"\"}}", encodedSecret) + + type fields struct { + client http.Client + baseURL string + config constants.CredentialProviderConfigOptions + } + type args struct { + path string + profile string + } + tests := []struct { + name string + fields fields + args args + patchResponse response + commitResponse response + wantErr bool + }{ + { + name: "working credential update", + fields: fields{ + client: http.Client{}, + config: constants.CredentialProviderConfigOptions{}, + }, + args: args{ + path: "testdata/testcreds", + profile: "eksa-packages", + }, + patchResponse: response{ + statusCode: http.StatusNoContent, + expectedBody: []byte(expectedBody), + responseMsg: "", + }, + commitResponse: response{ + statusCode: http.StatusOK, + responseMsg: "", + }, + wantErr: false, + }, + { + name: "commit credentials failed", + fields: fields{ + client: http.Client{}, + config: constants.CredentialProviderConfigOptions{}, + }, + args: args{ + path: "testdata/testcreds", + profile: "eksa-packages", + }, + patchResponse: response{ + statusCode: http.StatusNoContent, + expectedBody: []byte(expectedBody), + responseMsg: "", + }, + commitResponse: response{ + statusCode: http.StatusNotFound, + responseMsg: "", + }, + wantErr: true, + }, + { + name: "failed to patch data", + fields: fields{ + client: http.Client{}, + config: constants.CredentialProviderConfigOptions{}, + }, + args: args{ + path: "testdata/testcreds", + profile: "eksa-packages", + }, + patchResponse: response{ + statusCode: http.StatusNotFound, + expectedBody: []byte(expectedBody), + responseMsg: "", + }, + commitResponse: response{ + statusCode: http.StatusOK, + responseMsg: "", + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + svr := httptest.NewServer(http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodPatch { + validatePatchRequest(w, r, t, tt.patchResponse) + } else if r.Method == http.MethodPost { + w.WriteHeader(tt.commitResponse.statusCode) + fmt.Fprintf(w, tt.commitResponse.responseMsg) + } else { + t.Errorf("Recieved unexected request %v", r.Method) + } + }), + ) + b := &bottleRocket{ + client: tt.fields.client, + baseURL: svr.URL + "/", + config: tt.fields.config, + } + if err := b.UpdateAWSCredentials(tt.args.path, tt.args.profile); (err != nil) != tt.wantErr { + t.Errorf("UpdateAWSCredentials() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func Test_bottleRocket_UpdateCredentialProvider(t *testing.T) { + type fields struct { + client http.Client + baseURL string + config constants.CredentialProviderConfigOptions + } + + tests := []struct { + name string + fields fields + patchResponse response + wantErr bool + }{ + { + name: "default credential provider", + fields: fields{ + client: http.Client{}, + config: constants.CredentialProviderConfigOptions{ + ImagePatterns: []string{constants.DefaultImagePattern}, + DefaultCacheDuration: constants.DefaultCacheDuration, + }, + }, + patchResponse: response{ + statusCode: http.StatusNoContent, + expectedBody: []byte("{\"kubernetes\":{\"credential-providers\":{\"ecr-credential-provider\":{\"cache-duration\":\"30m\",\"enabled\":true,\"image-patterns\":[\"*.dkr.ecr.*.amazonaws.com\"]}}}}"), + responseMsg: "", + }, + wantErr: false, + }, + { + name: "non default values for credential provider", + fields: fields{ + client: http.Client{}, + config: constants.CredentialProviderConfigOptions{ + ImagePatterns: []string{"123456789.dkr.ecr.test-region.amazonaws.com"}, + DefaultCacheDuration: "24h", + }, + }, + patchResponse: response{ + statusCode: http.StatusNoContent, + expectedBody: []byte("{\"kubernetes\":{\"credential-providers\":{\"ecr-credential-provider\":{\"cache-duration\":\"24h\",\"enabled\":true,\"image-patterns\":[\"123456789.dkr.ecr.test-region.amazonaws.com\"]}}}}"), + responseMsg: "", + }, + wantErr: false, + }, + { + name: "multiple match images for credential provider", + fields: fields{ + client: http.Client{}, + config: constants.CredentialProviderConfigOptions{ + ImagePatterns: []string{"123456789.dkr.ecr.test-region.amazonaws.com", "987654321.dkr.ecr.test-region.amazonaws.com"}, + DefaultCacheDuration: "24h", + }, + }, + patchResponse: response{ + statusCode: http.StatusNoContent, + expectedBody: []byte("{\"kubernetes\":{\"credential-providers\":{\"ecr-credential-provider\":{\"cache-duration\":\"24h\",\"enabled\":true,\"image-patterns\":[\"123456789.dkr.ecr.test-region.amazonaws.com\",\"987654321.dkr.ecr.test-region.amazonaws.com\"]}}}}"), + responseMsg: "", + }, + wantErr: false, + }, + { + name: "failed credential provider update", + fields: fields{ + client: http.Client{}, + config: constants.CredentialProviderConfigOptions{ + ImagePatterns: []string{constants.DefaultImagePattern}, + DefaultCacheDuration: constants.DefaultCacheDuration, + }, + }, + patchResponse: response{ + statusCode: http.StatusNotFound, + expectedBody: []byte("{\"kubernetes\":{\"credential-providers\":{\"ecr-credential-provider\":{\"cache-duration\":\"30m\",\"enabled\":true,\"image-patterns\":[\"*.dkr.ecr.*.amazonaws.com\"]}}}}"), + responseMsg: "", + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + svr := httptest.NewServer(http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodPatch { + validatePatchRequest(w, r, t, tt.patchResponse) + } else { + t.Errorf("Recieved unexected request %v", r.Method) + } + }), + ) + + b := &bottleRocket{ + client: tt.fields.client, + baseURL: svr.URL + "/", + config: tt.fields.config, + } + if err := b.UpdateCredentialProvider(""); (err != nil) != tt.wantErr { + t.Errorf("UpdateCredentialProvider() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func validatePatchRequest(w http.ResponseWriter, r *http.Request, t *testing.T, patchResponse response) { + data, err := ioutil.ReadAll(r.Body) + if err != nil { + t.Errorf("failed to read response") + } + if !bytes.Equal(data, patchResponse.expectedBody) { + t.Errorf("Patch message expcted %v got %v", patchResponse.expectedBody, data) + } + w.WriteHeader(patchResponse.statusCode) + fmt.Fprintf(w, patchResponse.responseMsg) +} + +func Test_bottleRocket_Initialize(t *testing.T) { + type args struct { + config constants.CredentialProviderConfigOptions + } + tests := []struct { + name string + baseUrl string + args args + }{ + { + name: "simple initialization", + baseUrl: "http://localhost/", + args: args{ + config: constants.CredentialProviderConfigOptions{ + ImagePatterns: []string{constants.DefaultImagePattern}, + DefaultCacheDuration: constants.DefaultCacheDuration, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + b := &bottleRocket{} + b.Initialize(tt.args.config) + assert.Equal(t, tt.baseUrl, b.baseURL) + assert.Equal(t, tt.args.config, b.config) + assert.NotNil(t, b.client) + }) + } +} diff --git a/credentialproviderpackage/pkg/configurator/bottlerocket/testdata/testcreds b/credentialproviderpackage/pkg/configurator/bottlerocket/testdata/testcreds new file mode 100644 index 00000000..cc4ea03f --- /dev/null +++ b/credentialproviderpackage/pkg/configurator/bottlerocket/testdata/testcreds @@ -0,0 +1,3 @@ +[profile eksa-packages] +aws_access_key_id=AKIAIOSFODNN7EXAMPLE +aws_secret_access_key=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY diff --git a/credentialproviderpackage/pkg/configurator/configurator.go b/credentialproviderpackage/pkg/configurator/configurator.go new file mode 100644 index 00000000..cd28a06b --- /dev/null +++ b/credentialproviderpackage/pkg/configurator/configurator.go @@ -0,0 +1,17 @@ +package configurator + +import "credential-provider/pkg/constants" + +type Configurator interface { + // Initialize Handles node specific configuration depending on OS + Initialize(config constants.CredentialProviderConfigOptions) + + // UpdateAWSCredentials Handles AWS Credential Setup + UpdateAWSCredentials(sourcePath string, profile string) error + + // UpdateCredentialProvider Handles Credential Provider Setup + UpdateCredentialProvider(profile string) error + + // CommitChanges Applies changes to Kubelet + CommitChanges() error +} diff --git a/credentialproviderpackage/pkg/configurator/linux/linux.go b/credentialproviderpackage/pkg/configurator/linux/linux.go new file mode 100644 index 00000000..a493927f --- /dev/null +++ b/credentialproviderpackage/pkg/configurator/linux/linux.go @@ -0,0 +1,226 @@ +package linux + +import ( + _ "embed" + "fmt" + "io" + "io/ioutil" + "os" + "strings" + "syscall" + + ps "github.com/mitchellh/go-ps" + + "credential-provider/pkg/configurator" + "credential-provider/pkg/constants" + "credential-provider/pkg/log" + "credential-provider/pkg/templater" +) + +//go:embed templates/credential-provider-config.yaml +var credProviderTemplate string + +const ( + binPath = "/eksa-binaries/" + basePath = "/eksa-packages/" + credOutFile = "aws-creds" + mountedExtraArgs = "/node-files/kubelet-extra-args" + credProviderFile = "credential-provider-config.yaml" + + // Binaries + ecrCredProviderBinary = "ecr-credential-provider" + iamRolesSigningBinary = "aws_signing_helper" +) + +type linuxOS struct { + profile string + extraArgsPath string + basePath string + config constants.CredentialProviderConfigOptions +} + +var _ configurator.Configurator = (*linuxOS)(nil) + +func NewLinuxConfigurator() *linuxOS { + return &linuxOS{ + profile: "", + extraArgsPath: mountedExtraArgs, + basePath: basePath, + } +} + +func (c *linuxOS) Initialize(config constants.CredentialProviderConfigOptions) { + c.config = config +} + +func (c *linuxOS) UpdateAWSCredentials(sourcePath string, profile string) error { + c.profile = profile + dstPath := c.basePath + credOutFile + + err := copyWithPermissons(sourcePath, dstPath, 0600) + return err +} + +func (c *linuxOS) UpdateCredentialProvider(_ string) error { + // Adding to KUBELET_EXTRA_ARGS in place + file, err := ioutil.ReadFile(c.extraArgsPath) + if err != nil { + return err + } + + lines := strings.Split(string(file), "\n") + found := false + for i, line := range lines { + if strings.HasPrefix(line, "KUBELET_EXTRA_ARGS") { + found = true + args := c.updateKubeletArguments(line) + + if args != "" { + lines[i] = line + args + "\n" + } + } + } + if !found { + line := "KUBELET_EXTRA_ARGS=" + args := c.updateKubeletArguments(line) + if args != "" { + line = line + args + } + lines = append(lines, line) + } + + out := strings.Join(lines, "\n") + err = ioutil.WriteFile(c.extraArgsPath, []byte(out), 0644) + return err +} + +func (c *linuxOS) CommitChanges() error { + process, err := findKubeletProcess() + if err != nil { + return err + } + err = killProcess(process) + return err +} + +func killProcess(process ps.Process) error { + err := syscall.Kill(process.Pid(), syscall.SIGHUP) + return err +} + +func findKubeletProcess() (ps.Process, error) { + processList, err := ps.Processes() + if err != nil { + return nil, err + } + for x := range processList { + process := processList[x] + if process.Executable() == "kubelet" { + return process, nil + } + } + return nil, fmt.Errorf("cannot find Kubelet Process") +} + +func copyWithPermissons(srcpath, dstpath string, permission os.FileMode) (err error) { + r, err := os.Open(srcpath) + if err != nil { + return err + } + defer r.Close() // ok to ignore error: file was opened read-only. + + w, err := os.Create(dstpath) + if err != nil { + return err + } + + defer func() { + c := w.Close() + // Report the error from Close, if any. + // But do so only if there isn't already + // an outgoing error. + if c != nil && err == nil { + err = c + } + }() + + _, err = io.Copy(w, r) + if err != nil { + return err + } + err = os.Chmod(dstpath, permission) + return err +} + +func copyBinaries() (string, error) { + srcPath := binPath + ecrCredProviderBinary + dstPath := basePath + ecrCredProviderBinary + err := copyWithPermissons(srcPath, dstPath, 0700) + if err != nil { + return "", err + } + + err = os.Chmod(dstPath, 0700) + if err != nil { + return "", err + } + + srcPath = binPath + iamRolesSigningBinary + dstPath = basePath + iamRolesSigningBinary + err = copyWithPermissons(srcPath, dstPath, 0700) + if err != nil { + return "", err + } + + err = os.Chmod(dstPath, 0700) + if err != nil { + return "", err + } + return fmt.Sprintf(" --image-credential-provider-bin-dir=%s", basePath), nil +} + +func (c *linuxOS) createConfig() (string, error) { + values := map[string]interface{}{ + "profile": c.profile, + "config": basePath + credOutFile, + "home": basePath, + "imagePattern": c.config.ImagePatterns, + "cacheDuration": c.config.DefaultCacheDuration, + } + + dstPath := c.basePath + credProviderFile + + bytes, err := templater.Execute(credProviderTemplate, values) + if err != nil { + return "", nil + } + err = ioutil.WriteFile(dstPath, bytes, 0600) + if err != nil { + return "", err + } + return fmt.Sprintf(" --image-credential-provider-config=%s", dstPath), nil +} + +func (c *linuxOS) updateKubeletArguments(line string) string { + args := "" + if !strings.Contains(line, "KubeletCredentialProviders") { + args += " --feature-gates=KubeletCredentialProviders=true" + } + + if !strings.Contains(line, "image-credential-provider-config") { + val, err := c.createConfig() + if err != nil { + log.ErrorLogger.Printf("Error creating configuration %v", err) + } + args += val + + val, err = copyBinaries() + if err != nil { + log.ErrorLogger.Printf("Error coping binaries %v\n", err) + } + if !strings.Contains(line, "image-credential-provider-bin-dir") { + args += val + } + } + return args +} diff --git a/credentialproviderpackage/pkg/configurator/linux/linux_test.go b/credentialproviderpackage/pkg/configurator/linux/linux_test.go new file mode 100644 index 00000000..9ad90002 --- /dev/null +++ b/credentialproviderpackage/pkg/configurator/linux/linux_test.go @@ -0,0 +1,228 @@ +package linux + +import ( + "fmt" + "io/ioutil" + "os" + "testing" + + "github.com/stretchr/testify/assert" + + "credential-provider/internal/test" + "credential-provider/pkg/constants" +) + +func Test_linuxOS_updateKubeletArguments(t *testing.T) { + testDir, _ := test.NewWriter(t) + dir := testDir + "/" + type fields struct { + profile string + extraArgsPath string + basePath string + config constants.CredentialProviderConfigOptions + } + type args struct { + line string + } + tests := []struct { + name string + fields fields + args args + outputConfigPath string + configWantPath string + want string + }{ + { + name: "test empty string", + fields: fields{ + profile: "eksa-packages", + extraArgsPath: dir, + basePath: dir, + config: constants.CredentialProviderConfigOptions{ + ImagePatterns: []string{constants.DefaultImagePattern}, + DefaultCacheDuration: constants.DefaultCacheDuration, + }, + }, + args: args{line: ""}, + outputConfigPath: dir + "/" + credProviderFile, + configWantPath: "testdata/expected-config.yaml", + want: fmt.Sprintf(" --feature-gates=KubeletCredentialProviders=true "+ + "--image-credential-provider-config=%s%s", dir, credProviderFile), + }, + { + name: "test multiple match patterns", + fields: fields{ + profile: "eksa-packages", + extraArgsPath: dir, + basePath: dir, + config: constants.CredentialProviderConfigOptions{ + ImagePatterns: []string{"1234567.dkr.ecr.us-east-1.amazonaws.com", + "7654321.dkr.ecr.us-west-2.amazonaws.com"}, + DefaultCacheDuration: constants.DefaultCacheDuration, + }, + }, + args: args{line: ""}, + outputConfigPath: dir + "/" + credProviderFile, + configWantPath: "testdata/expected-config-multiple-patterns.yaml", + want: fmt.Sprintf(" --feature-gates=KubeletCredentialProviders=true "+ + "--image-credential-provider-config=%s%s", dir, credProviderFile), + }, + { + name: "skip credential provider if already provided", + fields: fields{ + profile: "eksa-packages", + extraArgsPath: dir, + basePath: dir, + config: constants.CredentialProviderConfigOptions{ + ImagePatterns: []string{constants.DefaultImagePattern}, + DefaultCacheDuration: constants.DefaultCacheDuration, + }, + }, + args: args{line: " --feature-gates=KubeletCredentialProviders=true"}, + outputConfigPath: dir + "/" + credProviderFile, + configWantPath: "testdata/expected-config.yaml", + want: fmt.Sprintf(" --image-credential-provider-config=%s%s", dir, credProviderFile), + }, + { + name: "skip both cred provider and feature gate if provided", + fields: fields{ + profile: "eksa-packages", + extraArgsPath: dir, + basePath: dir, + config: constants.CredentialProviderConfigOptions{ + ImagePatterns: []string{constants.DefaultImagePattern}, + DefaultCacheDuration: constants.DefaultCacheDuration, + }, + }, + args: args{line: " --feature-gates=KubeletCredentialProviders=false --image-credential-provider-config=blah"}, + outputConfigPath: dir + "/" + credProviderFile, + configWantPath: "", + want: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &linuxOS{ + profile: tt.fields.profile, + extraArgsPath: tt.fields.extraArgsPath, + basePath: tt.fields.basePath, + config: tt.fields.config, + } + if got := c.updateKubeletArguments(tt.args.line); got != tt.want { + t.Errorf("updateKubeletArguments() = %v, want %v", got, tt.want) + } + if tt.configWantPath != "" { + test.AssertFilesEquals(t, tt.outputConfigPath, tt.configWantPath) + } + + }) + } +} + +func Test_linuxOS_UpdateAWSCredentials(t *testing.T) { + testDir, _ := test.NewWriter(t) + dir := testDir + "/" + type fields struct { + profile string + extraArgsPath string + basePath string + config constants.CredentialProviderConfigOptions + } + type args struct { + sourcePath string + profile string + } + tests := []struct { + name string + fields fields + args args + wantErr bool + }{ + { + name: "simple credential move", + fields: fields{ + profile: "eksa-packages", + extraArgsPath: dir, + basePath: dir, + config: constants.CredentialProviderConfigOptions{ + ImagePatterns: []string{constants.DefaultImagePattern}, + DefaultCacheDuration: constants.DefaultCacheDuration, + }, + }, + args: args{ + sourcePath: "testdata/testcreds", + profile: "eksa-packages", + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dstFile := tt.fields.basePath + credOutFile + c := &linuxOS{ + profile: tt.fields.profile, + extraArgsPath: tt.fields.extraArgsPath, + basePath: tt.fields.basePath, + config: tt.fields.config, + } + if err := c.UpdateAWSCredentials(tt.args.sourcePath, tt.args.profile); (err != nil) != tt.wantErr { + t.Errorf("UpdateAWSCredentials() error = %v, wantErr %v", err, tt.wantErr) + } + info, err := os.Stat(dstFile) + if err != nil { + t.Errorf("Failed to open destination file") + } + if info.Mode().Perm() != os.FileMode(0600) { + t.Errorf("Credential file not saved with correct permission") + } + + if err != nil { + t.Errorf("Failed to set file back to readable") + } + expectedCreds, err := ioutil.ReadFile(tt.args.sourcePath) + if err != nil { + t.Errorf("Failed to read source credential file") + } + + actualCreds, err := ioutil.ReadFile(dstFile) + if err != nil { + t.Errorf("Failed to read created credential file") + } + assert.Equal(t, expectedCreds, actualCreds) + }) + } +} + +func Test_linuxOS_Initialize(t *testing.T) { + type fields struct { + profile string + extraArgsPath string + basePath string + config constants.CredentialProviderConfigOptions + } + type args struct { + config constants.CredentialProviderConfigOptions + } + tests := []struct { + name string + fields fields + args args + }{ + { + name: "simple initialization", + args: args{ + config: constants.CredentialProviderConfigOptions{ + ImagePatterns: []string{constants.DefaultImagePattern}, + DefaultCacheDuration: constants.DefaultCacheDuration, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := NewLinuxConfigurator() + c.Initialize(tt.args.config) + assert.Equal(t, c.config, tt.args.config) + }) + } +} diff --git a/credentialproviderpackage/pkg/configurator/linux/templates/credential-provider-config.yaml b/credentialproviderpackage/pkg/configurator/linux/templates/credential-provider-config.yaml new file mode 100644 index 00000000..3dc29279 --- /dev/null +++ b/credentialproviderpackage/pkg/configurator/linux/templates/credential-provider-config.yaml @@ -0,0 +1,17 @@ +apiVersion: kubelet.config.k8s.io/v1alpha1 +kind: CredentialProviderConfig +providers: + - name: ecr-credential-provider + matchImages:{{range $val := .imagePattern}} + - "{{$val}}"{{end}} + defaultCacheDuration: "{{.cacheDuration}}" + apiVersion: credentialprovider.kubelet.k8s.io/v1alpha1 + env: + - name: AWS_PROFILE + value: {{.profile}} + - name: AWS_CONFIG_FILE + value: {{.config}} + - name: PATH + value: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/eksa-packages + - name: HOME + value: {{.home}} diff --git a/credentialproviderpackage/pkg/configurator/linux/testdata/expected-config-multiple-patterns.yaml b/credentialproviderpackage/pkg/configurator/linux/testdata/expected-config-multiple-patterns.yaml new file mode 100644 index 00000000..41f9a587 --- /dev/null +++ b/credentialproviderpackage/pkg/configurator/linux/testdata/expected-config-multiple-patterns.yaml @@ -0,0 +1,18 @@ +apiVersion: kubelet.config.k8s.io/v1alpha1 +kind: CredentialProviderConfig +providers: + - name: ecr-credential-provider + matchImages: + - "1234567.dkr.ecr.us-east-1.amazonaws.com" + - "7654321.dkr.ecr.us-west-2.amazonaws.com" + defaultCacheDuration: "30m" + apiVersion: credentialprovider.kubelet.k8s.io/v1alpha1 + env: + - name: AWS_PROFILE + value: eksa-packages + - name: AWS_CONFIG_FILE + value: /eksa-packages/aws-creds + - name: PATH + value: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/eksa-packages + - name: HOME + value: /eksa-packages/ diff --git a/credentialproviderpackage/pkg/configurator/linux/testdata/expected-config.yaml b/credentialproviderpackage/pkg/configurator/linux/testdata/expected-config.yaml new file mode 100644 index 00000000..2c7ffcd1 --- /dev/null +++ b/credentialproviderpackage/pkg/configurator/linux/testdata/expected-config.yaml @@ -0,0 +1,17 @@ +apiVersion: kubelet.config.k8s.io/v1alpha1 +kind: CredentialProviderConfig +providers: + - name: ecr-credential-provider + matchImages: + - "*.dkr.ecr.*.amazonaws.com" + defaultCacheDuration: "30m" + apiVersion: credentialprovider.kubelet.k8s.io/v1alpha1 + env: + - name: AWS_PROFILE + value: eksa-packages + - name: AWS_CONFIG_FILE + value: /eksa-packages/aws-creds + - name: PATH + value: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/eksa-packages + - name: HOME + value: /eksa-packages/ diff --git a/credentialproviderpackage/pkg/configurator/linux/testdata/testcreds b/credentialproviderpackage/pkg/configurator/linux/testdata/testcreds new file mode 100644 index 00000000..cc4ea03f --- /dev/null +++ b/credentialproviderpackage/pkg/configurator/linux/testdata/testcreds @@ -0,0 +1,3 @@ +[profile eksa-packages] +aws_access_key_id=AKIAIOSFODNN7EXAMPLE +aws_secret_access_key=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY diff --git a/credentialproviderpackage/pkg/constants/constants.go b/credentialproviderpackage/pkg/constants/constants.go new file mode 100644 index 00000000..41a04ea2 --- /dev/null +++ b/credentialproviderpackage/pkg/constants/constants.go @@ -0,0 +1,12 @@ +package constants + +const ( + // Credential Provider constants + DefaultImagePattern = "*.dkr.ecr.*.amazonaws.com" + DefaultCacheDuration = "30m" +) + +type CredentialProviderConfigOptions struct { + ImagePatterns []string + DefaultCacheDuration string +} diff --git a/credentialproviderpackage/pkg/filewriter/filewriter.go b/credentialproviderpackage/pkg/filewriter/filewriter.go new file mode 100644 index 00000000..dde7c8a8 --- /dev/null +++ b/credentialproviderpackage/pkg/filewriter/filewriter.go @@ -0,0 +1,23 @@ +package filewriter + +import ( + "io" + "os" +) + +type FileWriter interface { + Write(fileName string, content []byte, f ...FileOptionsFunc) (path string, err error) + WithDir(dir string) (FileWriter, error) + CleanUp() + CleanUpTemp() + Dir() string + TempDir() string + Create(name string, f ...FileOptionsFunc) (_ io.WriteCloser, path string, _ error) +} + +type FileOptions struct { + IsTemp bool + Permissions os.FileMode +} + +type FileOptionsFunc func(op *FileOptions) diff --git a/credentialproviderpackage/pkg/filewriter/filewriter_defaults.go b/credentialproviderpackage/pkg/filewriter/filewriter_defaults.go new file mode 100644 index 00000000..9288400d --- /dev/null +++ b/credentialproviderpackage/pkg/filewriter/filewriter_defaults.go @@ -0,0 +1,19 @@ +package filewriter + +import ( + "os" +) + +const DefaultTmpFolder = "generated" + +func defaultFileOptions() *FileOptions { + return &FileOptions{true, os.ModePerm} +} + +func Permission0600(op *FileOptions) { + op.Permissions = 0o600 +} + +func PersistentFile(op *FileOptions) { + op.IsTemp = false +} diff --git a/credentialproviderpackage/pkg/filewriter/tmp_writer_test.go b/credentialproviderpackage/pkg/filewriter/tmp_writer_test.go new file mode 100644 index 00000000..e5443749 --- /dev/null +++ b/credentialproviderpackage/pkg/filewriter/tmp_writer_test.go @@ -0,0 +1,159 @@ +package filewriter_test + +import ( + "io/ioutil" + "os" + "path" + "path/filepath" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + + "credential-provider/pkg/filewriter" +) + +func TestTmpWriterWriteValid(t *testing.T) { + folder := "tmp_folder" + folder2 := "tmp_folder_2" + err := os.MkdirAll(folder2, os.ModePerm) + if err != nil { + t.Fatalf("error setting up test: %v", err) + } + defer os.RemoveAll(folder) + defer os.RemoveAll(folder2) + + tests := []struct { + testName string + dir string + fileName string + content []byte + }{ + { + testName: "dir doesn't exist", + dir: folder, + fileName: "TestTmpWriterWriteValid-success.yaml", + content: []byte(` + fake content + blablab + `), + }, + { + testName: "dir exists", + dir: folder2, + fileName: "test", + content: []byte(` + fake content + blablab + `), + }, + { + testName: "empty file name", + dir: folder, + fileName: "test", + content: []byte(` + fake content + blablab + `), + }, + } + for _, tt := range tests { + t.Run(tt.testName, func(t *testing.T) { + tr, err := filewriter.NewWriter(tt.dir) + if err != nil { + t.Fatalf("failed creating tmpWriter error = %v", err) + } + + gotPath, err := tr.Write(tt.fileName, tt.content) + if err != nil { + t.Fatalf("tmpWriter.Write() error = %v", err) + } + + if !strings.HasPrefix(gotPath, tt.dir) { + t.Errorf("tmpWriter.Write() = %v, want to start with %v", gotPath, tt.dir) + } + + if !strings.HasSuffix(gotPath, tt.fileName) { + t.Errorf("tmpWriter.Write() = %v, want to end with %v", gotPath, tt.fileName) + } + + content, err := ioutil.ReadFile(gotPath) + if err != nil { + t.Fatalf("error reading written file: %v", err) + } + + if string(content) != string(tt.content) { + t.Errorf("Write file content = %v, want %v", content, tt.content) + } + }) + } +} + +func TestTmpWriterWithDir(t *testing.T) { + rootFolder := "folder_root" + subFolder := "subFolder" + defer os.RemoveAll(rootFolder) + + tr, err := filewriter.NewWriter(rootFolder) + if err != nil { + t.Fatalf("failed creating tmpWriter error = %v", err) + } + + tr, err = tr.WithDir(subFolder) + if err != nil { + t.Fatalf("failed creating tmpWriter with subdir error = %v", err) + } + + gotPath, err := tr.Write("file.txt", []byte("file content")) + if err != nil { + t.Fatalf("tmpWriter.Write() error = %v", err) + } + + wantPathPrefix := filepath.Join(rootFolder, subFolder) + if !strings.HasPrefix(gotPath, wantPathPrefix) { + t.Errorf("tmpWriter.Write() = %v, want to start with %v", gotPath, wantPathPrefix) + } +} + +func TestCreate(t *testing.T) { + dir := t.TempDir() + const fileName = "test.txt" + + // Hard code the "generated". Its an implementation detail but we can't refactor it right now. + expectedPath := path.Join(dir, "generated", fileName) + expectedContent := []byte("test content") + + fr, err := filewriter.NewWriter(dir) + if err != nil { + t.Fatal(err) + } + + fh, path, err := fr.Create(fileName) + if err != nil { + t.Fatal(err) + } + + // We need to validate 2 things: (1) are the paths returned correct; (2) if we write content + // to the returned io.WriteCloser, is it written to the path also returened from the function. + + if path != expectedPath { + t.Fatalf("Received: %v; Expected: %v", path, expectedPath) + } + + if _, err := fh.Write(expectedContent); err != nil { + t.Fatal(err) + } + + if err := fh.Close(); err != nil { + t.Fatal(err) + } + + content, err := os.ReadFile(expectedPath) + if err != nil { + t.Fatal(err) + } + + if !cmp.Equal(content, expectedContent) { + t.Fatalf("Received: %v; Expected: %v", content, expectedContent) + } +} diff --git a/credentialproviderpackage/pkg/filewriter/writer.go b/credentialproviderpackage/pkg/filewriter/writer.go new file mode 100644 index 00000000..fe6239da --- /dev/null +++ b/credentialproviderpackage/pkg/filewriter/writer.go @@ -0,0 +1,99 @@ +package filewriter + +import ( + "errors" + "fmt" + "io" + "io/fs" + "io/ioutil" + "os" + "path/filepath" +) + +type writer struct { + dir string + tempDir string +} + +func NewWriter(dir string) (FileWriter, error) { + newFolder := filepath.Join(dir, DefaultTmpFolder) + if _, err := os.Stat(newFolder); errors.Is(err, os.ErrNotExist) { + err := os.MkdirAll(newFolder, os.ModePerm) + if err != nil { + return nil, fmt.Errorf("creating directory [%s]: %v", dir, err) + } + } + return &writer{dir: dir, tempDir: newFolder}, nil +} + +func (w *writer) Write(fileName string, content []byte, opts ...FileOptionsFunc) (string, error) { + o := buildOptions(w, opts) + + filePath := filepath.Join(o.BasePath, fileName) + err := ioutil.WriteFile(filePath, content, o.Permissions) + if err != nil { + return "", fmt.Errorf("writing to file [%s]: %v", filePath, err) + } + + return filePath, nil +} + +func (w *writer) WithDir(dir string) (FileWriter, error) { + return NewWriter(filepath.Join(w.dir, dir)) +} + +func (w *writer) Dir() string { + return w.dir +} + +func (w *writer) TempDir() string { + return w.tempDir +} + +func (w *writer) CleanUp() { + _, err := os.Stat(w.dir) + if err == nil { + os.RemoveAll(w.dir) + } +} + +func (w *writer) CleanUpTemp() { + _, err := os.Stat(w.tempDir) + if err == nil { + os.RemoveAll(w.tempDir) + } +} + +// Create creates a file with the given name rooted at w's base directory. +func (w *writer) Create(name string, opts ...FileOptionsFunc) (_ io.WriteCloser, path string, _ error) { + o := buildOptions(w, opts) + + path = filepath.Join(o.BasePath, name) + fh, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, o.Permissions) + return fh, path, err +} + +type options struct { + BasePath string + Permissions fs.FileMode +} + +// buildOptions converts a set of FileOptionsFunc's to a single options struct. +func buildOptions(w *writer, opts []FileOptionsFunc) options { + op := defaultFileOptions() + for _, fn := range opts { + fn(op) + } + + var basePath string + if op.IsTemp { + basePath = w.tempDir + } else { + basePath = w.dir + } + + return options{ + BasePath: basePath, + Permissions: op.Permissions, + } +} diff --git a/credentialproviderpackage/pkg/filewriter/writer_test.go b/credentialproviderpackage/pkg/filewriter/writer_test.go new file mode 100644 index 00000000..1eb084d2 --- /dev/null +++ b/credentialproviderpackage/pkg/filewriter/writer_test.go @@ -0,0 +1,203 @@ +package filewriter_test + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "testing" + + "credential-provider/internal/test" + "credential-provider/pkg/filewriter" +) + +func TestWriterWriteValid(t *testing.T) { + folder := "tmp_folder" + folder2 := "tmp_folder_2" + err := os.MkdirAll(folder2, os.ModePerm) + if err != nil { + t.Fatalf("error setting up test: %v", err) + } + defer os.RemoveAll(folder) + defer os.RemoveAll(folder2) + + tests := []struct { + testName string + dir string + fileName string + content []byte + }{ + { + testName: "test 1", + dir: folder, + fileName: "TestWriterWriteValid-success.yaml", + content: []byte(` + fake content + blablab + `), + }, + { + testName: "test 2", + dir: folder2, + fileName: "TestWriterWriteValid-success.yaml", + content: []byte(` + fake content + blablab + `), + }, + } + for _, tt := range tests { + t.Run(tt.testName, func(t *testing.T) { + tr, err := filewriter.NewWriter(tt.dir) + if err != nil { + t.Fatalf("failed creating writer error = %v", err) + } + + gotPath, err := tr.Write(tt.fileName, tt.content) + if err != nil { + t.Fatalf("writer.Write() error = %v", err) + } + + wantPath := filepath.Join(tt.dir, filewriter.DefaultTmpFolder, tt.fileName) + if strings.Compare(gotPath, wantPath) != 0 { + t.Errorf("writer.Write() = %v, want %v", gotPath, wantPath) + } + + test.AssertFilesEquals(t, gotPath, wantPath) + }) + } +} + +func TestEmptyFileName(t *testing.T) { + folder := "tmp_folder" + defer os.RemoveAll(folder) + tr, err := filewriter.NewWriter(folder) + if err != nil { + t.Fatalf("failed creating writer error = %v", err) + } + _, err = tr.Write("", []byte("content")) + if err == nil { + t.Fatalf("writer.Write() error is nil") + } +} + +func TestWriterWithDir(t *testing.T) { + rootFolder := "folder_root" + subFolder := "subFolder" + defer os.RemoveAll(rootFolder) + + tr, err := filewriter.NewWriter(rootFolder) + if err != nil { + t.Fatalf("failed creating writer error = %v", err) + } + + tr, err = tr.WithDir(subFolder) + if err != nil { + t.Fatalf("failed creating writer with subdir error = %v", err) + } + + gotPath, err := tr.Write("file.txt", []byte("file content")) + if err != nil { + t.Fatalf("writer.Write() error = %v", err) + } + + wantPathPrefix := filepath.Join(rootFolder, subFolder) + if !strings.HasPrefix(gotPath, wantPathPrefix) { + t.Errorf("writer.Write() = %v, want to start with %v", gotPath, wantPathPrefix) + } +} + +func TestWriterWritePersistent(t *testing.T) { + folder := "tmp_folder_opt" + folder2 := "tmp_folder_2_opt" + err := os.MkdirAll(folder2, os.ModePerm) + if err != nil { + t.Fatalf("error setting up test: %v", err) + } + defer os.RemoveAll(folder) + defer os.RemoveAll(folder2) + + tests := []struct { + testName string + dir string + fileName string + content []byte + options []filewriter.FileOptionsFunc + }{ + { + testName: "Write persistent file", + dir: folder, + fileName: "TestWriterWriteValid-success.yaml", + content: []byte(` + fake content + blablab + `), + options: []filewriter.FileOptionsFunc{filewriter.PersistentFile}, + }, + } + for _, tt := range tests { + t.Run(tt.testName, func(t *testing.T) { + tr, err := filewriter.NewWriter(tt.dir) + if err != nil { + t.Fatalf("failed creating writer error = %v", err) + } + + gotPath, err := tr.Write(tt.fileName, tt.content, tt.options...) + if err != nil { + t.Fatalf("writer.Write() error = %v", err) + } + + wantPath := filepath.Join(tt.dir, tt.fileName) + if strings.Compare(gotPath, wantPath) != 0 { + t.Errorf("writer.Write() = %v, want %v", gotPath, wantPath) + } + + test.AssertFilesEquals(t, gotPath, wantPath) + }) + } +} + +func TestWriterDir(t *testing.T) { + rootFolder := "folder_root" + defer os.RemoveAll(rootFolder) + + tr, err := filewriter.NewWriter(rootFolder) + if err != nil { + t.Fatalf("failed creating writer error = %v", err) + } + + if strings.Compare(tr.Dir(), rootFolder) != 0 { + t.Errorf("writer.Dir() = %v, want %v", tr.Dir(), rootFolder) + } +} + +func TestWriterTempDir(t *testing.T) { + rootFolder := "folder_root" + tempFolder := fmt.Sprintf("%s/generated", rootFolder) + defer os.RemoveAll(rootFolder) + + tr, err := filewriter.NewWriter(rootFolder) + if err != nil { + t.Fatalf("failed creating writer error = %v", err) + } + + if strings.Compare(tr.TempDir(), tempFolder) != 0 { + t.Errorf("writer.TempDir() = %v, want %v", tr.TempDir(), tempFolder) + } +} + +func TestWriterCleanUpTempDir(t *testing.T) { + rootFolder := "folder_root" + defer os.RemoveAll(rootFolder) + + tr, err := filewriter.NewWriter(rootFolder) + if err != nil { + t.Fatalf("failed creating writer error = %v", err) + } + + tr.CleanUpTemp() + + if _, err := os.Stat(tr.TempDir()); err == nil { + t.Errorf("writer.CleanUp(), want err, got nil") + } +} diff --git a/credentialproviderpackage/pkg/log/log.go b/credentialproviderpackage/pkg/log/log.go new file mode 100644 index 00000000..049453a2 --- /dev/null +++ b/credentialproviderpackage/pkg/log/log.go @@ -0,0 +1,18 @@ +package log + +import ( + "log" + "os" +) + +var ( + InfoLogger *log.Logger + WarningLogger *log.Logger + ErrorLogger *log.Logger +) + +func init() { + InfoLogger = log.New(os.Stdout, "INFO: ", log.Lshortfile) + WarningLogger = log.New(os.Stderr, "WARNING: ", log.Lshortfile) + ErrorLogger = log.New(os.Stderr, "ERROR: ", log.Lshortfile) +} diff --git a/credentialproviderpackage/pkg/templater/partialyaml.go b/credentialproviderpackage/pkg/templater/partialyaml.go new file mode 100644 index 00000000..053caf03 --- /dev/null +++ b/credentialproviderpackage/pkg/templater/partialyaml.go @@ -0,0 +1,29 @@ +package templater + +import ( + "reflect" + + "sigs.k8s.io/yaml" +) + +type PartialYaml map[string]interface{} + +func (p PartialYaml) AddIfNotZero(k string, v interface{}) { + if !isZeroVal(v) { + p[k] = v + } +} + +func isZeroVal(x interface{}) bool { + return x == nil || reflect.DeepEqual(x, reflect.Zero(reflect.TypeOf(x)).Interface()) +} + +func (p PartialYaml) ToYaml() (string, error) { + b, err := yaml.Marshal(p) + if err != nil { + return "", err + } + s := string(b) + + return s, nil +} diff --git a/credentialproviderpackage/pkg/templater/partialyaml_test.go b/credentialproviderpackage/pkg/templater/partialyaml_test.go new file mode 100644 index 00000000..464ed87e --- /dev/null +++ b/credentialproviderpackage/pkg/templater/partialyaml_test.go @@ -0,0 +1,131 @@ +package templater_test + +import ( + "reflect" + "testing" + + "credential-provider/internal/test" + "credential-provider/pkg/templater" +) + +func TestPartialYamlAddIfNotZero(t *testing.T) { + tests := []struct { + testName string + p templater.PartialYaml + k string + v interface{} + wantAdded bool + wantV interface{} + }{ + { + testName: "add string", + p: templater.PartialYaml{}, + k: "key", + v: "value", + wantAdded: true, + wantV: "value", + }, + { + testName: "add nil", + p: templater.PartialYaml{}, + k: "key", + v: nil, + wantAdded: false, + wantV: nil, + }, + { + testName: "add empty string", + p: templater.PartialYaml{}, + k: "key", + v: "", + wantAdded: false, + wantV: nil, + }, + { + testName: "add present string", + p: templater.PartialYaml{ + "key": "value_old", + }, + k: "key", + v: "value_new", + wantAdded: true, + wantV: "value_new", + }, + } + for _, tt := range tests { + t.Run(tt.testName, func(t *testing.T) { + tt.p.AddIfNotZero(tt.k, tt.v) + + gotV, gotAdded := tt.p[tt.k] + if tt.wantAdded != gotAdded { + t.Errorf("PartialYaml.AddIfNotZero() wasAdded = %v, wantAdded %v", gotAdded, tt.wantAdded) + } + + if !reflect.DeepEqual(gotV, tt.wantV) { + t.Errorf("PartialYaml.AddIfNotZero() gotValue = %v, wantValue %v", gotV, tt.wantV) + } + }) + } +} + +func TestPartialYamlToYaml(t *testing.T) { + tests := []struct { + testName string + p templater.PartialYaml + wantFile string + wantErr bool + }{ + { + testName: "simple object", + p: templater.PartialYaml{ + "key1": "value 1", + "key2": 2, + "key3": "value3", + }, + wantFile: "testdata/partial_yaml_object_expected.yaml", + wantErr: false, + }, + { + testName: "map", + p: templater.PartialYaml{ + "key1": "value 1", + "key2": 2, + "key3": map[string]string{ + "key_nest1": "value nest", + "key_nest2": "value nest 2", + }, + "key4": map[string]interface{}{ + "key_nest1": "value nest", + "key_nest2": 22, + }, + }, + wantFile: "testdata/partial_yaml_map_expected.yaml", + wantErr: false, + }, + { + testName: "array", + p: templater.PartialYaml{ + "key1": "value 1", + "key2": 2, + "key3": []string{"value array 1", "value array 2"}, + "key4": []interface{}{ + map[string]interface{}{ + "key_in_nest_array": "value", + "key_in_nest_array_2": 22, + }, + }, + }, + wantFile: "testdata/partial_yaml_array_expected.yaml", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.testName, func(t *testing.T) { + got, err := tt.p.ToYaml() + if (err != nil) != tt.wantErr { + t.Fatalf("PartialYaml.ToYaml() error = %v, wantErr %v", err, tt.wantErr) + } + test.AssertContentToFile(t, got, tt.wantFile) + }) + } +} diff --git a/credentialproviderpackage/pkg/templater/templater.go b/credentialproviderpackage/pkg/templater/templater.go new file mode 100644 index 00000000..437adb63 --- /dev/null +++ b/credentialproviderpackage/pkg/templater/templater.go @@ -0,0 +1,66 @@ +package templater + +import ( + "bytes" + "fmt" + "strings" + "text/template" + + "credential-provider/pkg/filewriter" +) + +type Templater struct { + writer filewriter.FileWriter +} + +func New(writer filewriter.FileWriter) *Templater { + return &Templater{ + writer: writer, + } +} + +func (t *Templater) WriteToFile(templateContent string, data interface{}, fileName string, f ...filewriter.FileOptionsFunc) (filePath string, err error) { + bytes, err := Execute(templateContent, data) + if err != nil { + return "", err + } + writtenFilePath, err := t.writer.Write(fileName, bytes, f...) + if err != nil { + return "", fmt.Errorf("writing template file: %v", err) + } + + return writtenFilePath, nil +} + +func (t *Templater) WriteBytesToFile(content []byte, fileName string, f ...filewriter.FileOptionsFunc) (filePath string, err error) { + writtenFilePath, err := t.writer.Write(fileName, content, f...) + if err != nil { + return "", fmt.Errorf("writing template file: %v", err) + } + + return writtenFilePath, nil +} + +func Execute(templateContent string, data interface{}) ([]byte, error) { + temp := template.New("tmpl") + funcMap := map[string]interface{}{ + "indent": func(spaces int, v string) string { + pad := strings.Repeat(" ", spaces) + return pad + strings.Replace(v, "\n", "\n"+pad, -1) + }, + "stringsJoin": strings.Join, + } + temp = temp.Funcs(funcMap) + + temp, err := temp.Parse(templateContent) + if err != nil { + return nil, fmt.Errorf("parsing template: %v", err) + } + + var buf bytes.Buffer + err = temp.Execute(&buf, data) + if err != nil { + return nil, fmt.Errorf("substituting values for template: %v", err) + } + return buf.Bytes(), nil +} diff --git a/credentialproviderpackage/pkg/templater/templater_test.go b/credentialproviderpackage/pkg/templater/templater_test.go new file mode 100644 index 00000000..f6c74386 --- /dev/null +++ b/credentialproviderpackage/pkg/templater/templater_test.go @@ -0,0 +1,143 @@ +package templater_test + +import ( + "os" + "strings" + "testing" + + "credential-provider/internal/test" + "credential-provider/pkg/filewriter" + "credential-provider/pkg/templater" +) + +func TestTemplaterWriteToFileSuccess(t *testing.T) { + type dataStruct struct { + Key1, Key2, Key3, KeyAndValue3 string + Conditional bool + } + + tests := []struct { + testName string + templateFile string + data dataStruct + fileName string + wantFilePath string + wantErr bool + }{ + { + testName: "with conditional true", + templateFile: "testdata/test1_template.yaml", + data: dataStruct{ + Key1: "value_1", + Key2: "value_2", + Key3: "value_3", + Conditional: true, + }, + fileName: "file_tmp.yaml", + wantFilePath: "testdata/test1_conditional_true_want.yaml", + wantErr: false, + }, + { + testName: "with conditional false", + templateFile: "testdata/test1_template.yaml", + data: dataStruct{ + Key1: "value_1", + Key2: "value_2", + Key3: "value_3", + Conditional: false, + }, + fileName: "file_tmp.yaml", + wantFilePath: "testdata/test1_conditional_false_want.yaml", + wantErr: false, + }, + { + testName: "with indent", + templateFile: "testdata/test_indent_template.yaml", + data: dataStruct{ + Key1: "value_1", + Key2: "value_2", + KeyAndValue3: "key3: value_3", + Conditional: true, + }, + fileName: "file_tmp.yaml", + wantFilePath: "testdata/test_indent_want.yaml", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.testName, func(t *testing.T) { + _, writer := test.NewWriter(t) + tr := templater.New(writer) + templateContent := test.ReadFile(t, tt.templateFile) + gotFilePath, err := tr.WriteToFile(templateContent, tt.data, tt.fileName) + if (err != nil) != tt.wantErr { + t.Fatalf("Templater.WriteToFile() error = %v, wantErr %v", err, tt.wantErr) + } + + if !strings.HasSuffix(gotFilePath, tt.fileName) { + t.Errorf("Templater.WriteToFile() = %v, want to end with %v", gotFilePath, tt.fileName) + } + + test.AssertFilesEquals(t, gotFilePath, tt.wantFilePath) + }) + } +} + +func TestTemplaterWriteToFileError(t *testing.T) { + folder := "tmp_folder" + defer os.RemoveAll(folder) + + writer, err := filewriter.NewWriter(folder) + if err != nil { + t.Fatalf("failed creating writer error = #{err}") + } + + type dataStruct struct { + Key1, Key2, Key3 string + Conditional bool + } + + tests := []struct { + testName string + templateFile string + data dataStruct + fileName string + }{ + { + testName: "invalid template", + templateFile: "testdata/invalid_template.yaml", + data: dataStruct{ + Key1: "value_1", + Key2: "value_2", + Key3: "value_3", + Conditional: true, + }, + fileName: "file_tmp.yaml", + }, + { + testName: "data doesn't exist", + templateFile: "testdata/key4_template.yaml", + data: dataStruct{ + Key1: "value_1", + Key2: "value_2", + Key3: "value_3", + Conditional: false, + }, + fileName: "file_tmp.yaml", + }, + } + for _, tt := range tests { + t.Run(tt.testName, func(t *testing.T) { + tr := templater.New(writer) + templateContent := test.ReadFile(t, tt.templateFile) + gotFilePath, err := tr.WriteToFile(templateContent, tt.data, tt.fileName) + if err == nil { + t.Errorf("Templater.WriteToFile() error = nil") + } + + if gotFilePath != "" { + t.Errorf("Templater.WriteToFile() = %v, want nil", gotFilePath) + } + }) + } +} diff --git a/credentialproviderpackage/pkg/templater/testdata/invalid_template.yaml b/credentialproviderpackage/pkg/templater/testdata/invalid_template.yaml new file mode 100644 index 00000000..d4010323 --- /dev/null +++ b/credentialproviderpackage/pkg/templater/testdata/invalid_template.yaml @@ -0,0 +1,5 @@ +key1: {{ .Key1 }} +key2: {{ .Key2 +{{ if .Conditional }} +key3: {{ .Key3 }} +{{ end }} diff --git a/credentialproviderpackage/pkg/templater/testdata/key4_template.yaml b/credentialproviderpackage/pkg/templater/testdata/key4_template.yaml new file mode 100644 index 00000000..1cb80cf9 --- /dev/null +++ b/credentialproviderpackage/pkg/templater/testdata/key4_template.yaml @@ -0,0 +1,6 @@ +key1: {{ .Key1 }} +key2: {{ .Key2 }} +{{ if .Conditional }} +key3: {{ .Key3 }} +{{ end }} +key4: {{ .Key4 }} diff --git a/credentialproviderpackage/pkg/templater/testdata/partial_yaml_array_expected.yaml b/credentialproviderpackage/pkg/templater/testdata/partial_yaml_array_expected.yaml new file mode 100644 index 00000000..3d6ff659 --- /dev/null +++ b/credentialproviderpackage/pkg/templater/testdata/partial_yaml_array_expected.yaml @@ -0,0 +1,8 @@ +key1: value 1 +key2: 2 +key3: +- value array 1 +- value array 2 +key4: +- key_in_nest_array: value + key_in_nest_array_2: 22 diff --git a/credentialproviderpackage/pkg/templater/testdata/partial_yaml_map_expected.yaml b/credentialproviderpackage/pkg/templater/testdata/partial_yaml_map_expected.yaml new file mode 100644 index 00000000..e8948147 --- /dev/null +++ b/credentialproviderpackage/pkg/templater/testdata/partial_yaml_map_expected.yaml @@ -0,0 +1,8 @@ +key1: value 1 +key2: 2 +key3: + key_nest1: value nest + key_nest2: value nest 2 +key4: + key_nest1: value nest + key_nest2: 22 diff --git a/credentialproviderpackage/pkg/templater/testdata/partial_yaml_object_expected.yaml b/credentialproviderpackage/pkg/templater/testdata/partial_yaml_object_expected.yaml new file mode 100644 index 00000000..c3c4509a --- /dev/null +++ b/credentialproviderpackage/pkg/templater/testdata/partial_yaml_object_expected.yaml @@ -0,0 +1,3 @@ +key1: value 1 +key2: 2 +key3: value3 diff --git a/credentialproviderpackage/pkg/templater/testdata/test1_conditional_false_want.yaml b/credentialproviderpackage/pkg/templater/testdata/test1_conditional_false_want.yaml new file mode 100644 index 00000000..cedb76a4 --- /dev/null +++ b/credentialproviderpackage/pkg/templater/testdata/test1_conditional_false_want.yaml @@ -0,0 +1,3 @@ +key1: value_1 +key2: value_2 + diff --git a/credentialproviderpackage/pkg/templater/testdata/test1_conditional_true_want.yaml b/credentialproviderpackage/pkg/templater/testdata/test1_conditional_true_want.yaml new file mode 100644 index 00000000..7116c47d --- /dev/null +++ b/credentialproviderpackage/pkg/templater/testdata/test1_conditional_true_want.yaml @@ -0,0 +1,5 @@ +key1: value_1 +key2: value_2 + +key3: value_3 + diff --git a/credentialproviderpackage/pkg/templater/testdata/test1_template.yaml b/credentialproviderpackage/pkg/templater/testdata/test1_template.yaml new file mode 100644 index 00000000..9e60bf61 --- /dev/null +++ b/credentialproviderpackage/pkg/templater/testdata/test1_template.yaml @@ -0,0 +1,5 @@ +key1: {{ .Key1 }} +key2: {{ .Key2 }} +{{ if .Conditional }} +key3: {{ .Key3 }} +{{ end }} diff --git a/credentialproviderpackage/pkg/templater/testdata/test_indent_template.yaml b/credentialproviderpackage/pkg/templater/testdata/test_indent_template.yaml new file mode 100644 index 00000000..d8bfae53 --- /dev/null +++ b/credentialproviderpackage/pkg/templater/testdata/test_indent_template.yaml @@ -0,0 +1,5 @@ +key1: {{ .Key1 }} +key2: {{ .Key2 }} +{{ if .Conditional }} +{{ .KeyAndValue3 | indent 2 }} +{{ end }} diff --git a/credentialproviderpackage/pkg/templater/testdata/test_indent_want.yaml b/credentialproviderpackage/pkg/templater/testdata/test_indent_want.yaml new file mode 100644 index 00000000..94a0244c --- /dev/null +++ b/credentialproviderpackage/pkg/templater/testdata/test_indent_want.yaml @@ -0,0 +1,5 @@ +key1: value_1 +key2: value_2 + + key3: value_3 + diff --git a/credentialproviderpackage/pkg/templater/yaml.go b/credentialproviderpackage/pkg/templater/yaml.go new file mode 100644 index 00000000..42e2ee7f --- /dev/null +++ b/credentialproviderpackage/pkg/templater/yaml.go @@ -0,0 +1,39 @@ +package templater + +import ( + "fmt" + + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/yaml" +) + +const objectSeparator string = "\n---\n" + +func AppendYamlResources(resources ...[]byte) []byte { + separator := []byte(objectSeparator) + + size := 0 + for _, resource := range resources { + size += len(resource) + len(separator) + } + + b := make([]byte, 0, size) + for _, resource := range resources { + b = append(b, resource...) + b = append(b, separator...) + } + + return b +} + +func ObjectsToYaml(objs ...runtime.Object) ([]byte, error) { + r := [][]byte{} + for _, o := range objs { + b, err := yaml.Marshal(o) + if err != nil { + return nil, fmt.Errorf("failed to marshal object: %v", err) + } + r = append(r, b) + } + return AppendYamlResources(r...), nil +} diff --git a/credentialproviderpackage/skaffold.yaml b/credentialproviderpackage/skaffold.yaml new file mode 100644 index 00000000..ee11c0d6 --- /dev/null +++ b/credentialproviderpackage/skaffold.yaml @@ -0,0 +1,24 @@ +apiVersion: skaffold/v3 +kind: Config +metadata: + name: credential-provider +build: + tagPolicy: + envTemplate: + template: "{{.EMPTY}}" + artifacts: + - image: credentialpackage + docker: + dockerfile: Dockerfile +manifests: + helm: + releases: + - name: credential-provider-helm + chartPath: charts/credential-provider-package + setValueTemplates: + image.registry: "{{.ECR_PUBLIC_REGISTRY}}" + image.repository: "credentialpackage" + image.tag: "{{.IMAGE_DIGEST_credentialpackage}}" + image.digest: "{{.IMAGE_DIGEST_credentialpackage}}" + sourceRegistry: "{{.SKAFFOLD_DEFAULT_REPO}}" +