diff --git a/README.md b/README.md index d55b45767..bf6afde31 100644 --- a/README.md +++ b/README.md @@ -630,6 +630,40 @@ The default is `false`. For more details, please run the command and check the examples to apply to your shell. +### Annotate a Kubernetes manifest + +To add or modify dapr annotations on an existing Kubernetes manifest, use the `dapr annotate` command: + +```bash +dapr annotate [flags] mydeployment.yaml +``` + +This will add the `dapr.io/enabled` and the `dapr.io/app-id` annotations. The dapr app id will be genereated using the format `--` where the values are taken from the existing Kubernetes object metadata. + +To provide your own dapr app id, provide the flag `--app-id`. + +All dapr annotations are available to set if a value is provided for the appropriate flag on the `dapr annotate` command. + +You can also provide the Kubernetes manifest via stdin: + +```bash +kubectl get deploy mydeploy -o yaml | dapr annotate - | kubectl apply -f - +``` + +Or you can provide the Kubernetes manifest via a URL: + +```bash +dapr annotate --log-level debug https://raw.githubusercontent.com/dapr/quickstarts/master/tutorials/hello-kubernetes/deploy/node.yaml | kubectl apply -f - +``` + +If the input contains multiple manifests then the command will search for the first appropriate one to apply the annotations. If you'd rather it applied to a specific manifest then you can provide the `--resource` flag with the value set to the name of the object you'd like to apply the annotations to. If you have a conflict between namespaces you can also provide the namespace via the `--namespace` flag to isolate the manifest you wish to target. + +If you want to annotate multiple manifests, you can chain together the `dapr annotate` commands with each applying the annotation to a specific manifest. + +```bash +kubectl get deploy -o yaml | dapr annotate -r nodeapp --log-level debug - | dapr annotate --log-level debug -r pythonapp - | kubectl apply -f - +``` + ## Reference for the Dapr CLI See the [Reference Guide](https://docs.dapr.io/reference/cli/) for more information about individual Dapr commands. diff --git a/cmd/annotate.go b/cmd/annotate.go new file mode 100644 index 000000000..31aa2990f --- /dev/null +++ b/cmd/annotate.go @@ -0,0 +1,359 @@ +/* +Copyright 2021 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmd + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "os" + "path/filepath" + + "github.com/spf13/cobra" + + "github.com/dapr/cli/pkg/kubernetes" + "github.com/dapr/cli/pkg/print" +) + +var ( + annotateTargetResource string + annotateTargetNamespace string + annotateAppID string + annotateAppPort int + annotateConfig string + annotateAppProtocol string + annotateEnableProfile bool + annotateLogLevel string + annotateAPITokenSecret string + annotateAppTokenSecret string + annotateLogAsJSON bool + annotateAppMaxConcurrency int + annotateEnableMetrics bool + annotateMetricsPort int + annotateEnableDebug bool + annotateEnv string + annotateCPULimit string + annotateMemoryLimit string + annotateCPURequest string + annotateMemoryRequest string + annotateListenAddresses string + annotateLivenessProbeDelay int + annotateLivenessProbeTimeout int + annotateLivenessProbePeriod int + annotateLivenessProbeThreshold int + annotateReadinessProbeDelay int + annotateReadinessProbeTimeout int + annotateReadinessProbePeriod int + annotateReadinessProbeThreshold int + annotateDaprImage string + annotateAppSSL bool + annotateMaxRequestBodySize int + annotateHTTPStreamRequestBody bool + annotateGracefulShutdownSeconds int +) + +var AnnotateCmd = &cobra.Command{ + Use: "annotate [flags] CONFIG-FILE", + Short: "Add dapr annotations to a Kubernetes configuration. Supported platforms: Kubernetes", + Example: ` +# Annotate the first deployment found in the input +kubectl get deploy -l app=node -o yaml | dapr annotate - | kubectl apply -f - + +# Annotate multiple deployments by name in a chain +kubectl get deploy -o yaml | dapr annotate -r nodeapp - | dapr annotate -r pythonapp - | kubectl apply -f - + +# Annotate deployment in a specific namespace from file or directory by name +dapr annotate -r nodeapp -n namespace mydeploy.yaml | kubectl apply -f - + +# Annotate deployment from url by name +dapr annotate -r nodeapp --log-level debug https://raw.githubusercontent.com/dapr/quickstarts/master/tutorials/hello-kubernetes/deploy/node.yaml | kubectl apply -f - + +-------------------------------------------------------------------------------- +WARNING: If an app id is not provided, we will generate one using the format '--'. +-------------------------------------------------------------------------------- +`, + Run: func(cmd *cobra.Command, args []string) { + if len(args) < 1 { + print.FailureStatusEvent(os.Stderr, "please specify a kubernetes resource file") + os.Exit(1) + } + + input, err := readInput(args[0]) + if err != nil { + print.FailureStatusEvent(os.Stderr, err.Error()) + os.Exit(1) + } + + var config kubernetes.K8sAnnotatorConfig + if annotateTargetResource != "" { + config = kubernetes.K8sAnnotatorConfig{ + TargetResource: &annotateTargetResource, + } // nolint:exhaustivestruct + if annotateTargetNamespace != "" { + config.TargetNamespace = &annotateTargetNamespace + } + } else { + if annotateTargetNamespace != "" { + // The resource is empty but namespace is set, this + // is invalid as we cannot search for a resource + // if the identifier isn't provided. + print.FailureStatusEvent(os.Stderr, "--resource is required when --namespace is provided.") + os.Exit(1) + } + } + annotator := kubernetes.NewK8sAnnotator(config) + opts := getOptionsFromFlags() + if err := annotator.Annotate(input, os.Stdout, opts); err != nil { + print.FailureStatusEvent(os.Stderr, err.Error()) + os.Exit(1) + } + }, +} + +func readInput(arg string) ([]io.Reader, error) { + var inputs []io.Reader + var err error + if arg == "-" { + // input is from stdin. + inputs = append(inputs, os.Stdin) + } else if isURL(arg) { + inputs, err = readInputsFromURL(arg) + if err != nil { + return nil, err + } + } else { + // input is from file or dir. + inputs, err = readInputsFromFS(arg) + if err != nil { + return nil, err + } + } + + return inputs, nil +} + +func readInputsFromURL(url string) ([]io.Reader, error) { + resp, err := http.Get(url) // #nosec + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unable to read from %s: %d - %s", url, resp.StatusCode, resp.Status) + } + + var b []byte + b, err = ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + reader := bytes.NewReader(b) + return []io.Reader{reader}, nil +} + +func isURL(maybeURL string) bool { + url, err := url.ParseRequestURI(maybeURL) + if err != nil { + return false + } + + return url.Host != "" && url.Scheme != "" +} + +func readInputsFromFS(path string) ([]io.Reader, error) { + stat, err := os.Stat(path) + if err != nil { + return nil, err + } + + if !stat.IsDir() { + // input is a file. + var file *os.File + file, err = os.Open(path) + if err != nil { + return nil, err + } + + return []io.Reader{file}, nil + } + + // input is a directory. + var inputs []io.Reader + err = filepath.Walk(path, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + if info.IsDir() { + return nil + } + + file, err := os.Open(path) + if err != nil { + return err + } + + inputs = append(inputs, file) + return nil + }) + + if err != nil { + return nil, err + } + + return inputs, nil +} + +func getOptionsFromFlags() kubernetes.AnnotateOptions { + // TODO: Use a pointer for int flag where zero is nil not -1. + o := []kubernetes.AnnoteOption{} + if annotateAppID != "" { + o = append(o, kubernetes.WithAppID(annotateAppID)) + } + if annotateConfig != "" { + o = append(o, kubernetes.WithConfig(annotateConfig)) + } + if annotateAppPort != -1 { + o = append(o, kubernetes.WithAppPort(annotateAppPort)) + } + if annotateAppProtocol != "" { + o = append(o, kubernetes.WithAppProtocol(annotateAppProtocol)) + } + if annotateEnableProfile { + o = append(o, kubernetes.WithProfileEnabled()) + } + if annotateLogLevel != "" { + o = append(o, kubernetes.WithLogLevel(annotateLogLevel)) + } + if annotateAPITokenSecret != "" { + o = append(o, kubernetes.WithAPITokenSecret(annotateAPITokenSecret)) + } + if annotateAppTokenSecret != "" { + o = append(o, kubernetes.WithAppTokenSecret(annotateAppTokenSecret)) + } + if annotateLogAsJSON { + o = append(o, kubernetes.WithLogAsJSON()) + } + if annotateAppMaxConcurrency != -1 { + o = append(o, kubernetes.WithAppMaxConcurrency(annotateAppMaxConcurrency)) + } + if annotateEnableMetrics { + o = append(o, kubernetes.WithMetricsEnabled()) + } + if annotateMetricsPort != -1 { + o = append(o, kubernetes.WithMetricsPort(annotateMetricsPort)) + } + if annotateEnableDebug { + o = append(o, kubernetes.WithDebugEnabled()) + } + if annotateEnv != "" { + o = append(o, kubernetes.WithEnv(annotateEnv)) + } + if annotateCPULimit != "" { + o = append(o, kubernetes.WithCPULimit(annotateCPULimit)) + } + if annotateMemoryLimit != "" { + o = append(o, kubernetes.WithMemoryLimit(annotateMemoryLimit)) + } + if annotateCPURequest != "" { + o = append(o, kubernetes.WithCPURequest(annotateCPURequest)) + } + if annotateMemoryRequest != "" { + o = append(o, kubernetes.WithMemoryRequest(annotateMemoryRequest)) + } + if annotateListenAddresses != "" { + o = append(o, kubernetes.WithListenAddresses(annotateListenAddresses)) + } + if annotateLivenessProbeDelay != -1 { + o = append(o, kubernetes.WithLivenessProbeDelay(annotateLivenessProbeDelay)) + } + if annotateLivenessProbeTimeout != -1 { + o = append(o, kubernetes.WithLivenessProbeTimeout(annotateLivenessProbeTimeout)) + } + if annotateLivenessProbePeriod != -1 { + o = append(o, kubernetes.WithLivenessProbePeriod(annotateLivenessProbePeriod)) + } + if annotateLivenessProbeThreshold != -1 { + o = append(o, kubernetes.WithLivenessProbeThreshold(annotateLivenessProbeThreshold)) + } + if annotateReadinessProbeDelay != -1 { + o = append(o, kubernetes.WithReadinessProbeDelay(annotateReadinessProbeDelay)) + } + if annotateReadinessProbeTimeout != -1 { + o = append(o, kubernetes.WithReadinessProbeTimeout(annotateReadinessProbeTimeout)) + } + if annotateReadinessProbePeriod != -1 { + o = append(o, kubernetes.WithReadinessProbePeriod(annotateReadinessProbePeriod)) + } + if annotateReadinessProbeThreshold != -1 { + o = append(o, kubernetes.WithReadinessProbeThreshold(annotateReadinessProbeThreshold)) + } + if annotateDaprImage != "" { + o = append(o, kubernetes.WithDaprImage(annotateDaprImage)) + } + if annotateAppSSL { + o = append(o, kubernetes.WithAppSSL()) + } + if annotateMaxRequestBodySize != -1 { + o = append(o, kubernetes.WithMaxRequestBodySize(annotateMaxRequestBodySize)) + } + if annotateHTTPStreamRequestBody { + o = append(o, kubernetes.WithHTTPStreamRequestBody()) + } + if annotateGracefulShutdownSeconds != -1 { + o = append(o, kubernetes.WithGracefulShutdownSeconds(annotateGracefulShutdownSeconds)) + } + return kubernetes.NewAnnotateOptions(o...) +} + +func init() { + AnnotateCmd.Flags().StringVarP(&annotateTargetResource, "resource", "r", "", "The resource to target to annotate") + AnnotateCmd.Flags().StringVarP(&annotateTargetNamespace, "namespace", "n", "", "The namespace the resource target is in (can only be set if --resource is also set)") + AnnotateCmd.Flags().StringVarP(&annotateAppID, "app-id", "a", "", "The app id to annotate") + AnnotateCmd.Flags().IntVarP(&annotateAppPort, "app-port", "p", -1, "The port to expose the app on") + AnnotateCmd.Flags().StringVarP(&annotateConfig, "config", "c", "", "The config file to annotate") + AnnotateCmd.Flags().StringVar(&annotateAppProtocol, "app-protocol", "", "The protocol to use for the app") + AnnotateCmd.Flags().BoolVar(&annotateEnableProfile, "enable-profile", false, "Enable profiling") + AnnotateCmd.Flags().StringVar(&annotateLogLevel, "log-level", "", "The log level to use") + AnnotateCmd.Flags().StringVar(&annotateAPITokenSecret, "api-token-secret", "", "The secret to use for the API token") + AnnotateCmd.Flags().StringVar(&annotateAppTokenSecret, "app-token-secret", "", "The secret to use for the app token") + AnnotateCmd.Flags().BoolVar(&annotateLogAsJSON, "log-as-json", false, "Log as JSON") + AnnotateCmd.Flags().IntVar(&annotateAppMaxConcurrency, "app-max-concurrency", -1, "The maximum number of concurrent requests to allow") + AnnotateCmd.Flags().BoolVar(&annotateEnableMetrics, "enable-metrics", false, "Enable metrics") + AnnotateCmd.Flags().IntVar(&annotateMetricsPort, "metrics-port", -1, "The port to expose the metrics on") + AnnotateCmd.Flags().BoolVar(&annotateEnableDebug, "enable-debug", false, "Enable debug") + AnnotateCmd.Flags().StringVar(&annotateEnv, "env", "", "Environment variables to set (key value pairs, comma separated)") + AnnotateCmd.Flags().StringVar(&annotateCPULimit, "cpu-limit", "", "The CPU limit to set") + AnnotateCmd.Flags().StringVar(&annotateMemoryLimit, "memory-limit", "", "The memory limit to set") + AnnotateCmd.Flags().StringVar(&annotateCPURequest, "cpu-request", "", "The CPU request to set") + AnnotateCmd.Flags().StringVar(&annotateMemoryRequest, "memory-request", "", "The memory request to set") + AnnotateCmd.Flags().StringVar(&annotateListenAddresses, "listen-addresses", "", "The addresses to listen on") + AnnotateCmd.Flags().IntVar(&annotateLivenessProbeDelay, "liveness-probe-delay", -1, "The delay to use for the liveness probe") + AnnotateCmd.Flags().IntVar(&annotateLivenessProbeTimeout, "liveness-probe-timeout", -1, "The timeout to use for the liveness probe") + AnnotateCmd.Flags().IntVar(&annotateLivenessProbePeriod, "liveness-probe-period", -1, "The period to use for the liveness probe") + AnnotateCmd.Flags().IntVar(&annotateLivenessProbeThreshold, "liveness-probe-threshold", -1, "The threshold to use for the liveness probe") + AnnotateCmd.Flags().IntVar(&annotateReadinessProbeDelay, "readiness-probe-delay", -1, "The delay to use for the readiness probe") + AnnotateCmd.Flags().IntVar(&annotateReadinessProbeTimeout, "readiness-probe-timeout", -1, "The timeout to use for the readiness probe") + AnnotateCmd.Flags().IntVar(&annotateReadinessProbePeriod, "readiness-probe-period", -1, "The period to use for the readiness probe") + AnnotateCmd.Flags().IntVar(&annotateReadinessProbeThreshold, "readiness-probe-threshold", -1, "The threshold to use for the readiness probe") + AnnotateCmd.Flags().StringVar(&annotateDaprImage, "dapr-image", "", "The image to use for the dapr sidecar container") + AnnotateCmd.Flags().BoolVar(&annotateAppSSL, "app-ssl", false, "Enable SSL for the app") + AnnotateCmd.Flags().IntVar(&annotateMaxRequestBodySize, "max-request-body-size", -1, "The maximum request body size to use") + AnnotateCmd.Flags().BoolVar(&annotateHTTPStreamRequestBody, "http-stream-request-body", false, "Enable streaming request body for HTTP") + AnnotateCmd.Flags().IntVar(&annotateGracefulShutdownSeconds, "graceful-shutdown-seconds", -1, "The number of seconds to wait for the app to shutdown") + RootCmd.AddCommand(AnnotateCmd) +} diff --git a/go.mod b/go.mod index c61cdd47c..f6cda1c34 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,8 @@ module github.com/dapr/cli go 1.17 require ( + github.com/Azure/go-autorest/autorest v0.11.23 // indirect + github.com/Azure/go-autorest/autorest/adal v0.9.16 // indirect github.com/Pallinder/sillyname-go v0.0.0-20130730142914-97aeae9e6ba1 github.com/briandowns/spinner v1.6.1 github.com/dapr/dapr v1.7.0 @@ -30,14 +32,15 @@ require ( k8s.io/cli-runtime v0.23.4 k8s.io/client-go v0.23.4 k8s.io/helm v2.16.10+incompatible + sigs.k8s.io/yaml v1.3.0 ) +require github.com/evanphx/json-patch v4.12.0+incompatible + require ( cloud.google.com/go v0.99.0 // indirect github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect github.com/Azure/go-autorest v14.2.0+incompatible // indirect - github.com/Azure/go-autorest/autorest v0.11.23 // indirect - github.com/Azure/go-autorest/autorest/adal v0.9.16 // indirect github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect github.com/Azure/go-autorest/logger v0.2.1 // indirect github.com/Azure/go-autorest/tracing v0.6.0 // indirect @@ -52,6 +55,7 @@ require ( github.com/PuerkitoBio/purell v1.1.1 // indirect github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/StackExchange/wmi v0.0.0-20210224194228-fe8f1750fd46 // indirect + github.com/andybalholm/brotli v1.0.2 // indirect github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect @@ -67,7 +71,6 @@ require ( github.com/docker/go-connections v0.4.0 // indirect github.com/docker/go-metrics v0.0.1 // indirect github.com/docker/go-units v0.4.0 // indirect - github.com/evanphx/json-patch v4.12.0+incompatible // indirect github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect github.com/fsnotify/fsnotify v1.5.1 // indirect github.com/ghodss/yaml v1.0.0 // indirect @@ -80,6 +83,7 @@ require ( github.com/gobwas/glob v0.2.3 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v4 v4.0.0 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/google/btree v1.0.1 // indirect github.com/google/go-cmp v0.5.6 // indirect @@ -99,6 +103,7 @@ require ( github.com/jmoiron/sqlx v1.3.4 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/kelseyhightower/envconfig v1.4.0 // indirect github.com/klauspost/compress v1.14.4 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect @@ -147,10 +152,14 @@ require ( github.com/subosito/gotenv v1.2.0 // indirect github.com/tklauser/go-sysconf v0.3.6 // indirect github.com/tklauser/numcpus v0.2.2 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasthttp v1.31.1-0.20211216042702-258a4c17b4f4 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca // indirect + go.opencensus.io v0.23.0 // indirect + go.opentelemetry.io/otel v0.20.0 // indirect go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838 // indirect golang.org/x/net v0.0.0-20220107192237-5cfca573fb4d // indirect @@ -178,7 +187,6 @@ require ( sigs.k8s.io/kustomize/api v0.10.1 // indirect sigs.k8s.io/kustomize/kyaml v0.13.0 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect - sigs.k8s.io/yaml v1.3.0 // indirect ) replace ( diff --git a/go.sum b/go.sum index afb340334..368ea8f1d 100644 --- a/go.sum +++ b/go.sum @@ -146,6 +146,8 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/alexflint/go-filemutex v0.0.0-20171022225611-72bdc8eae2ae/go.mod h1:CgnQgUtFrFz9mxFNtED3jI5tLDjKlOM+oUF/sTk6ps0= +github.com/andybalholm/brotli v1.0.2 h1:JKnhI/XQ75uFBTiuzXpzFrUriDPiZjlOSzh6wXogP0E= +github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20210826220005-b48c857c3a0e/go.mod h1:F7bn7fEU90QkQ3tnmaTx3LTKLEDqnwWODIYppRQ5hnY= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= @@ -706,12 +708,15 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/karrick/godirwalk v1.15.8 h1:7+rWAZPn9zuRxaIqqT8Ohs2Q2Ac0msBqwRdxNCr2VVs= github.com/karrick/godirwalk v1.15.8/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk= +github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= +github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= 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/klauspost/compress v1.11.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.14.4 h1:eijASRJcobkVtSt81Olfh7JX43osYLwy5krOJo6YEu4= github.com/klauspost/compress v1.14.4/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= @@ -1081,6 +1086,11 @@ github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.31.1-0.20211216042702-258a4c17b4f4 h1:UKbv1Y0TRLKcgacl2+v4xPt3iJLhjP0RCGwMOkUW1ko= +github.com/valyala/fasthttp v1.31.1-0.20211216042702-258a4c17b4f4/go.mod h1:2rsYD01CKFrjjsvFxx75KlEUNpWNBY9JWD3K/7o2Cus= +github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= github.com/vishvananda/netlink v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk= github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= github.com/vishvananda/netlink v1.1.1-0.20201029203352-d40f9887b852/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= @@ -1141,6 +1151,7 @@ go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opentelemetry.io/contrib v0.20.0/go.mod h1:G/EtFaa6qaN7+LxqfIAT3GiZa7Wv5DTBUzl5H4LY0Kc= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.20.0/go.mod h1:oVGt1LRbBOBq1A5BQLlUg9UaU/54aiHw8cgjV3aWZ/E= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.20.0/go.mod h1:2AboqHi0CiIZU0qwhtUfCYD1GeUzvvIXWNkhDt7ZMG4= +go.opentelemetry.io/otel v0.20.0 h1:eaP0Fqu7SXHwvjiqDq83zImeehOHX8doTvU9AwXON8g= go.opentelemetry.io/otel v0.20.0/go.mod h1:Y3ugLH2oa81t5QO+Lty+zXf8zC9L26ax4Nzoxm/dooo= go.opentelemetry.io/otel/exporters/otlp v0.20.0/go.mod h1:YIieizyaN77rtLJra0buKiNBOm9XQfkPEKBeuhoMwAM= go.opentelemetry.io/otel/metric v0.20.0/go.mod h1:598I5tYlH1vzBjn+BTuhzTCSb/9debfNp6R3s7Pr1eU= @@ -1181,6 +1192,7 @@ golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= @@ -1275,6 +1287,7 @@ golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLd golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= diff --git a/pkg/kubernetes/annotator.go b/pkg/kubernetes/annotator.go new file mode 100644 index 000000000..1116b3f8b --- /dev/null +++ b/pkg/kubernetes/annotator.go @@ -0,0 +1,495 @@ +package kubernetes + +import ( + "bufio" + "encoding/json" + "errors" + "fmt" + "io" + "strconv" + "strings" + + jsonpatch "github.com/evanphx/json-patch" + appsv1 "k8s.io/api/apps/v1" + batchv1 "k8s.io/api/batch/v1" + batchv1beta1 "k8s.io/api/batch/v1beta1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + yamlDecoder "k8s.io/apimachinery/pkg/util/yaml" + "sigs.k8s.io/yaml" + + "github.com/dapr/dapr/pkg/injector" +) + +const ( + // Dapr annotation keys. + daprEnabledKey = "dapr.io/enabled" + daprAppPortKey = "dapr.io/app-port" + daprConfigKey = "dapr.io/config" + daprAppProtocolKey = "dapr.io/app-protocol" + daprAppIDKey = "dapr.io/app-id" + daprEnableProfilingKey = "dapr.io/enable-profiling" + daprLogLevelKey = "dapr.io/log-level" + daprAPITokenSecretKey = "dapr.io/api-token-secret" /* #nosec */ + daprAppTokenSecretKey = "dapr.io/app-token-secret" /* #nosec */ + daprLogAsJSONKey = "dapr.io/log-as-json" + daprAppMaxConcurrencyKey = "dapr.io/app-max-concurrency" + daprEnableMetricsKey = "dapr.io/enable-metrics" + daprMetricsPortKey = "dapr.io/metrics-port" + daprEnableDebugKey = "dapr.io/enable-debug" + daprDebugPortKey = "dapr.io/debug-port" + daprEnvKey = "dapr.io/env" + daprCPULimitKey = "dapr.io/sidecar-cpu-limit" + daprMemoryLimitKey = "dapr.io/sidecar-memory-limit" + daprCPURequestKey = "dapr.io/sidecar-cpu-request" + daprMemoryRequestKey = "dapr.io/sidecar-memory-request" + daprListenAddressesKey = "dapr.io/sidecar-listen-addresses" + daprLivenessProbeDelayKey = "dapr.io/sidecar-liveness-probe-delay-seconds" + daprLivenessProbeTimeoutKey = "dapr.io/sidecar-liveness-probe-timeout-seconds" + daprLivenessProbePeriodKey = "dapr.io/sidecar-liveness-probe-period-seconds" + daprLivenessProbeThresholdKey = "dapr.io/sidecar-liveness-probe-threshold" + daprReadinessProbeDelayKey = "dapr.io/sidecar-readiness-probe-delay-seconds" + daprReadinessProbeTimeoutKey = "dapr.io/sidecar-readiness-probe-timeout-seconds" + daprReadinessProbePeriodKey = "dapr.io/sidecar-readiness-probe-period-seconds" + daprReadinessProbeThresholdKey = "dapr.io/sidecar-readiness-probe-threshold" + daprImageKey = "dapr.io/sidecar-image" + daprAppSSLKey = "dapr.io/app-ssl" + daprMaxRequestBodySizeKey = "dapr.io/http-max-request-size" + daprReadBufferSizeKey = "dapr.io/http-read-buffer-size" + daprHTTPStreamRequestBodyKey = "dapr.io/http-stream-request-body" + daprGracefulShutdownSecondsKey = "dapr.io/graceful-shutdown-seconds" + + // K8s kinds. + pod = "pod" + deployment = "deployment" + replicaset = "replicaset" + daemonset = "daemonset" + statefulset = "statefulset" + cronjob = "cronjob" + job = "job" + list = "list" + + cronjobAnnotationsPath = "/spec/jobTemplate/spec/template/metadata/annotations" + podAnnotationsPath = "/metadata/annotations" + templateAnnotationsPath = "/spec/template/metadata/annotations" +) + +type Annotator interface { + Annotate(io.Reader, io.Writer) error +} + +type K8sAnnotator struct { + config K8sAnnotatorConfig + annotated bool +} + +type K8sAnnotatorConfig struct { + // If TargetResource is set, we will search for it and then inject + // annotations on that target resource. If it is not set, we will + // update the first appropriate resource we find. + TargetResource *string + // If TargetNamespace is set, we will search for the target resource + // in the provided target namespace. If it is not set, we will + // just search for the first occurrence of the target resource. + TargetNamespace *string +} + +func NewK8sAnnotator(config K8sAnnotatorConfig) *K8sAnnotator { + return &K8sAnnotator{ + config: config, + annotated: false, + } +} + +// Annotate injects dapr annotations into the kubernetes resource. +func (p *K8sAnnotator) Annotate(inputs []io.Reader, out io.Writer, opts AnnotateOptions) error { + for _, input := range inputs { + err := p.processInput(input, out, opts) + if err != nil { + return err + } + } + + return nil +} + +func (p *K8sAnnotator) processInput(input io.Reader, out io.Writer, opts AnnotateOptions) error { + reader := yamlDecoder.NewYAMLReader(bufio.NewReaderSize(input, 4096)) + + var result []byte + iterations := 0 + // Read from input and process until EOF or error. + for { + bytes, err := reader.Read() + if errors.Is(err, io.EOF) { + break + } + if err != nil { + return err + } + + // Check if the input is a list as + // these requires additional processing. + var metaType metav1.TypeMeta + if err = yaml.Unmarshal(bytes, &metaType); err != nil { + return err + } + + kind := strings.ToLower(metaType.Kind) + if kind == list { + var sourceList corev1.List + if err = yaml.Unmarshal(bytes, &sourceList); err != nil { + return err + } + items := []runtime.RawExtension{} + for _, item := range sourceList.Items { + var processedYAML []byte + processedYAML, err = p.processYAML(item.Raw, opts) + if err != nil { + return err + } + var annotatedJSON []byte + annotatedJSON, err = yaml.YAMLToJSON(processedYAML) + if err != nil { + return err + } + items = append(items, runtime.RawExtension{Raw: annotatedJSON}) // nolint:exhaustivestruct + } + sourceList.Items = items + result, err = yaml.Marshal(sourceList) + if err != nil { + return err + } + } else { + var processedYAML []byte + processedYAML, err = p.processYAML(bytes, opts) + if err != nil { + return err + } + result = processedYAML + } + + // Insert separator between documents. + if iterations > 0 { + out.Write([]byte("---\n")) + } + + // Write result from processing into the writer. + _, err = out.Write(result) + if err != nil { + return err + } + + iterations++ + } + + return nil +} + +func (p *K8sAnnotator) processYAML(yamlBytes []byte, opts AnnotateOptions) ([]byte, error) { + var err error + var processedYAML []byte + if p.annotated { + // We can only inject dapr into a single resource per execution as the configuration + // options are scoped to a single resource e.g. app-id, port, etc. are specific to a + // dapr enabled resource. Instead we expect multiple runs to be piped together. + processedYAML = yamlBytes + } else { + var annotated bool + processedYAML, annotated, err = p.annotateYAML(yamlBytes, opts) + if err != nil { + return nil, err + } + if annotated { + // Record that we have injected into a document. + p.annotated = annotated + } + } + return processedYAML, nil +} + +func (p *K8sAnnotator) annotateYAML(input []byte, config AnnotateOptions) ([]byte, bool, error) { + // We read the metadata again here so this method is encapsulated. + var metaType metav1.TypeMeta + if err := yaml.Unmarshal(input, &metaType); err != nil { + return nil, false, err + } + + // If the input resource is a 'kind' that + // we want to inject dapr into, then we + // Unmarshal the input into the appropriate + // type and set the required fields to build + // a patch (path, value, op). + var path string + var annotations map[string]string + var name string + var ns string + + kind := strings.ToLower(metaType.Kind) + switch kind { + case pod: + pod := &corev1.Pod{} // nolint:exhaustivestruct + if err := yaml.Unmarshal(input, pod); err != nil { + return nil, false, err + } + name = pod.Name + annotations = pod.Annotations + path = podAnnotationsPath + ns = getNamespaceOrDefault(pod) + case cronjob: + cronjob := &batchv1beta1.CronJob{} // nolint:exhaustivestruct + if err := yaml.Unmarshal(input, cronjob); err != nil { + return nil, false, err + } + name = cronjob.Name + annotations = cronjob.Spec.JobTemplate.Spec.Template.Annotations + path = cronjobAnnotationsPath + ns = getNamespaceOrDefault(cronjob) + case deployment: + deployment := &appsv1.Deployment{} // nolint:exhaustivestruct + if err := yaml.Unmarshal(input, deployment); err != nil { + return nil, false, err + } + name = deployment.Name + annotations = deployment.Spec.Template.Annotations + path = templateAnnotationsPath + ns = getNamespaceOrDefault(deployment) + case replicaset: + replicaset := &appsv1.ReplicaSet{} // nolint:exhaustivestruct + if err := yaml.Unmarshal(input, replicaset); err != nil { + return nil, false, err + } + name = replicaset.Name + annotations = replicaset.Spec.Template.Annotations + path = templateAnnotationsPath + ns = getNamespaceOrDefault(replicaset) + case job: + job := &batchv1.Job{} // nolint:exhaustivestruct + if err := yaml.Unmarshal(input, job); err != nil { + return nil, false, err + } + name = job.Name + annotations = job.Spec.Template.Annotations + path = templateAnnotationsPath + ns = getNamespaceOrDefault(job) + case statefulset: + statefulset := &appsv1.StatefulSet{} // nolint:exhaustivestruct + if err := yaml.Unmarshal(input, statefulset); err != nil { + return nil, false, err + } + name = statefulset.Name + annotations = statefulset.Spec.Template.Annotations + path = templateAnnotationsPath + ns = getNamespaceOrDefault(statefulset) + case daemonset: + daemonset := &appsv1.DaemonSet{} // nolint:exhaustivestruct + if err := yaml.Unmarshal(input, daemonset); err != nil { + return nil, false, err + } + name = daemonset.Name + annotations = daemonset.Spec.Template.Annotations + path = templateAnnotationsPath + ns = getNamespaceOrDefault(daemonset) + default: + // No annotation needed for this kind. + return input, false, nil + } + + // TODO: Currently this is where we decide not to + // annotate dapr on this resource as it isn't the + // target we are looking for. This is a bit late + // so it would be good to find a earlier place to + // do this. + if p.config.TargetResource != nil && *p.config.TargetResource != "" { + if !strings.EqualFold(*p.config.TargetResource, name) { + // Not the resource name we're annotating. + return input, false, nil + } + if p.config.TargetNamespace != nil && *p.config.TargetNamespace != "" { + if !strings.EqualFold(*p.config.TargetNamespace, ns) { + // Not the namespace we're annotating. + return input, false, nil + } + } + } + + // Get the dapr annotations and set them on the + // resources existing annotation map. This will + // override any existing conflicting annotations. + if annotations == nil { + annotations = make(map[string]string) + } + daprAnnotations := getDaprAnnotations(&config) + for k, v := range daprAnnotations { + // TODO: Should we log when we are overwriting? + // if _, exists := annotations[k]; exists {}. + annotations[k] = v + } + + // Check if the app id has been set, if not, we'll + // use the resource metadata namespace, kind and name. + // For example: namespace-kind-name. + if _, appIDSet := annotations[daprAppIDKey]; !appIDSet { + annotations[daprAppIDKey] = fmt.Sprintf("%s-%s-%s", ns, kind, name) + } + + // Create a patch operation for the annotations. + patchOps := []injector.PatchOperation{} + patchOps = append(patchOps, injector.PatchOperation{ + Op: "add", + Path: path, + Value: annotations, + }) + patchBytes, err := json.Marshal(patchOps) + if err != nil { + return nil, false, err + } + if len(patchBytes) == 0 { + return input, false, nil + } + patch, err := jsonpatch.DecodePatch(patchBytes) + if err != nil { + return nil, false, err + } + + // As we are applying the patch as a json patch, + // we have to convert the current YAML resource to + // JSON, apply the patch and then convert back. + inputAsJSON, err := yaml.YAMLToJSON(input) + if err != nil { + return nil, false, err + } + annotatedAsJSON, err := patch.Apply(inputAsJSON) + if err != nil { + return nil, false, err + } + annotatedAsYAML, err := yaml.JSONToYAML(annotatedAsJSON) + if err != nil { + return nil, false, err + } + + return annotatedAsYAML, true, nil +} + +type NamespacedObject interface { + GetNamespace() string +} + +func getNamespaceOrDefault(obj NamespacedObject) string { + ns := obj.GetNamespace() + if ns == "" { + return "default" + } + return ns +} + +func getDaprAnnotations(config *AnnotateOptions) map[string]string { + annotations := make(map[string]string) + + annotations[daprEnabledKey] = "true" + if config.appID != nil { + annotations[daprAppIDKey] = *config.appID + } + if config.metricsEnabled != nil { + annotations[daprEnableMetricsKey] = strconv.FormatBool(*config.metricsEnabled) + } + if config.metricsPort != nil { + annotations[daprMetricsPortKey] = strconv.FormatInt(int64(*config.metricsPort), 10) + } + if config.appPort != nil { + annotations[daprAppPortKey] = strconv.FormatInt(int64(*config.appPort), 10) + } + if config.config != nil { + annotations[daprConfigKey] = *config.config + } + if config.appProtocol != nil { + annotations[daprAppProtocolKey] = *config.appProtocol + } + if config.profileEnabled != nil { + annotations[daprEnableProfilingKey] = strconv.FormatBool(*config.profileEnabled) + } + if config.logLevel != nil { + annotations[daprLogLevelKey] = *config.logLevel + } + if config.logAsJSON != nil { + annotations[daprLogAsJSONKey] = strconv.FormatBool(*config.logAsJSON) + } + if config.apiTokenSecret != nil { + annotations[daprAPITokenSecretKey] = *config.apiTokenSecret + } + if config.appTokenSecret != nil { + annotations[daprAppTokenSecretKey] = *config.appTokenSecret + } + if config.appMaxConcurrency != nil { + annotations[daprAppMaxConcurrencyKey] = strconv.FormatInt(int64(*config.appMaxConcurrency), 10) + } + if config.debugEnabled != nil { + annotations[daprEnableDebugKey] = strconv.FormatBool(*config.debugEnabled) + } + if config.debugPort != nil { + annotations[daprDebugPortKey] = strconv.FormatInt(int64(*config.debugPort), 10) + } + if config.env != nil { + annotations[daprEnvKey] = *config.env + } + if config.cpuLimit != nil { + annotations[daprCPULimitKey] = *config.cpuLimit + } + if config.memoryLimit != nil { + annotations[daprMemoryLimitKey] = *config.memoryLimit + } + if config.cpuRequest != nil { + annotations[daprCPURequestKey] = *config.cpuRequest + } + if config.memoryRequest != nil { + annotations[daprMemoryRequestKey] = *config.memoryRequest + } + if config.listenAddresses != nil { + annotations[daprListenAddressesKey] = *config.listenAddresses + } + if config.livenessProbeDelay != nil { + annotations[daprLivenessProbeDelayKey] = strconv.FormatInt(int64(*config.livenessProbeDelay), 10) + } + if config.livenessProbeTimeout != nil { + annotations[daprLivenessProbeTimeoutKey] = strconv.FormatInt(int64(*config.livenessProbeTimeout), 10) + } + if config.livenessProbePeriod != nil { + annotations[daprLivenessProbePeriodKey] = strconv.FormatInt(int64(*config.livenessProbePeriod), 10) + } + if config.livenessProbeThreshold != nil { + annotations[daprLivenessProbeThresholdKey] = strconv.FormatInt(int64(*config.livenessProbeThreshold), 10) + } + if config.readinessProbeDelay != nil { + annotations[daprReadinessProbeDelayKey] = strconv.FormatInt(int64(*config.readinessProbeDelay), 10) + } + if config.readinessProbeTimeout != nil { + annotations[daprReadinessProbeTimeoutKey] = strconv.FormatInt(int64(*config.readinessProbeTimeout), 10) + } + if config.readinessProbePeriod != nil { + annotations[daprReadinessProbePeriodKey] = strconv.FormatInt(int64(*config.readinessProbePeriod), 10) + } + if config.readinessProbeThreshold != nil { + annotations[daprReadinessProbeThresholdKey] = strconv.FormatInt(int64(*config.readinessProbeThreshold), 10) + } + if config.image != nil { + annotations[daprImageKey] = *config.image + } + if config.appSSL != nil { + annotations[daprAppSSLKey] = strconv.FormatBool(*config.appSSL) + } + if config.maxRequestBodySize != nil { + annotations[daprMaxRequestBodySizeKey] = strconv.FormatInt(int64(*config.maxRequestBodySize), 10) + } + if config.readBufferSize != nil { + annotations[daprReadBufferSizeKey] = strconv.FormatInt(int64(*config.readBufferSize), 10) + } + if config.httpStreamRequestBody != nil { + annotations[daprHTTPStreamRequestBodyKey] = strconv.FormatBool(*config.httpStreamRequestBody) + } + if config.gracefulShutdownSeconds != nil { + annotations[daprGracefulShutdownSecondsKey] = strconv.FormatInt(int64(*config.gracefulShutdownSeconds), 10) + } + + return annotations +} diff --git a/pkg/kubernetes/annotator_config.go b/pkg/kubernetes/annotator_config.go new file mode 100644 index 000000000..edad666f5 --- /dev/null +++ b/pkg/kubernetes/annotator_config.go @@ -0,0 +1,259 @@ +package kubernetes + +// AnnotateOptions configure the injection behavior. +type AnnotateOptions struct { + appID *string + metricsEnabled *bool + metricsPort *int + appPort *int + config *string + appProtocol *string + profileEnabled *bool + logLevel *string + apiTokenSecret *string + appTokenSecret *string + logAsJSON *bool + appMaxConcurrency *int + debugEnabled *bool + debugPort *int + env *string + cpuLimit *string + memoryLimit *string + cpuRequest *string + memoryRequest *string + listenAddresses *string + livenessProbeDelay *int + livenessProbeTimeout *int + livenessProbePeriod *int + livenessProbeThreshold *int + readinessProbeDelay *int + readinessProbeTimeout *int + readinessProbePeriod *int + readinessProbeThreshold *int + image *string + appSSL *bool + maxRequestBodySize *int + readBufferSize *int + httpStreamRequestBody *bool + gracefulShutdownSeconds *int +} + +type AnnoteOption func(*AnnotateOptions) + +func NewAnnotateOptions(opts ...AnnoteOption) AnnotateOptions { + config := AnnotateOptions{} // nolint:exhaustivestruct + for _, opt := range opts { + opt(&config) + } + return config +} + +func WithAppID(appID string) AnnoteOption { + return func(config *AnnotateOptions) { + config.appID = &appID + } +} + +func WithMetricsEnabled() AnnoteOption { + return func(config *AnnotateOptions) { + enabled := true + config.metricsEnabled = &enabled + } +} + +func WithMetricsPort(port int) AnnoteOption { + return func(config *AnnotateOptions) { + config.metricsPort = &port + } +} + +func WithAppPort(port int) AnnoteOption { + return func(config *AnnotateOptions) { + config.appPort = &port + } +} + +func WithConfig(cfg string) AnnoteOption { + return func(config *AnnotateOptions) { + config.config = &cfg + } +} + +func WithAppProtocol(protocol string) AnnoteOption { + return func(config *AnnotateOptions) { + config.appProtocol = &protocol + } +} + +func WithProfileEnabled() AnnoteOption { + return func(config *AnnotateOptions) { + enabled := true + config.profileEnabled = &enabled + } +} + +func WithLogLevel(logLevel string) AnnoteOption { + return func(config *AnnotateOptions) { + config.logLevel = &logLevel + } +} + +func WithAPITokenSecret(apiTokenSecret string) AnnoteOption { + return func(config *AnnotateOptions) { + config.apiTokenSecret = &apiTokenSecret + } +} + +func WithAppTokenSecret(appTokenSecret string) AnnoteOption { + return func(config *AnnotateOptions) { + config.appTokenSecret = &appTokenSecret + } +} + +func WithLogAsJSON() AnnoteOption { + return func(config *AnnotateOptions) { + enabled := true + config.logAsJSON = &enabled + } +} + +func WithAppMaxConcurrency(maxConcurrency int) AnnoteOption { + return func(config *AnnotateOptions) { + config.appMaxConcurrency = &maxConcurrency + } +} + +func WithDebugEnabled() AnnoteOption { + return func(config *AnnotateOptions) { + enabled := true + config.debugEnabled = &enabled + } +} + +func WithDebugPort(debugPort int) AnnoteOption { + return func(config *AnnotateOptions) { + config.debugPort = &debugPort + } +} + +func WithEnv(env string) AnnoteOption { + return func(config *AnnotateOptions) { + config.env = &env + } +} + +func WithCPULimit(cpuLimit string) AnnoteOption { + return func(config *AnnotateOptions) { + config.cpuLimit = &cpuLimit + } +} + +func WithMemoryLimit(memoryLimit string) AnnoteOption { + return func(config *AnnotateOptions) { + config.memoryLimit = &memoryLimit + } +} + +func WithCPURequest(cpuRequest string) AnnoteOption { + return func(config *AnnotateOptions) { + config.cpuRequest = &cpuRequest + } +} + +func WithMemoryRequest(memoryRequest string) AnnoteOption { + return func(config *AnnotateOptions) { + config.memoryRequest = &memoryRequest + } +} + +func WithListenAddresses(listenAddresses string) AnnoteOption { + return func(config *AnnotateOptions) { + config.listenAddresses = &listenAddresses + } +} + +func WithLivenessProbeDelay(livenessProbeDelay int) AnnoteOption { + return func(config *AnnotateOptions) { + config.livenessProbeDelay = &livenessProbeDelay + } +} + +func WithLivenessProbeTimeout(livenessProbeTimeout int) AnnoteOption { + return func(config *AnnotateOptions) { + config.livenessProbeTimeout = &livenessProbeTimeout + } +} + +func WithLivenessProbePeriod(livenessProbePeriod int) AnnoteOption { + return func(config *AnnotateOptions) { + config.livenessProbePeriod = &livenessProbePeriod + } +} + +func WithLivenessProbeThreshold(livenessProbeThreshold int) AnnoteOption { + return func(config *AnnotateOptions) { + config.livenessProbeThreshold = &livenessProbeThreshold + } +} + +func WithReadinessProbeDelay(readinessProbeDelay int) AnnoteOption { + return func(config *AnnotateOptions) { + config.readinessProbeDelay = &readinessProbeDelay + } +} + +func WithReadinessProbeTimeout(readinessProbeTimeout int) AnnoteOption { + return func(config *AnnotateOptions) { + config.readinessProbeTimeout = &readinessProbeTimeout + } +} + +func WithReadinessProbePeriod(readinessProbePeriod int) AnnoteOption { + return func(config *AnnotateOptions) { + config.readinessProbePeriod = &readinessProbePeriod + } +} + +func WithReadinessProbeThreshold(readinessProbeThreshold int) AnnoteOption { + return func(config *AnnotateOptions) { + config.readinessProbeThreshold = &readinessProbeThreshold + } +} + +func WithDaprImage(image string) AnnoteOption { + return func(config *AnnotateOptions) { + config.image = &image + } +} + +func WithAppSSL() AnnoteOption { + return func(config *AnnotateOptions) { + enabled := true + config.appSSL = &enabled + } +} + +func WithMaxRequestBodySize(maxRequestBodySize int) AnnoteOption { + return func(config *AnnotateOptions) { + config.maxRequestBodySize = &maxRequestBodySize + } +} + +func WithReadBufferSize(readBufferSize int) AnnoteOption { + return func(config *AnnotateOptions) { + config.readBufferSize = &readBufferSize + } +} + +func WithHTTPStreamRequestBody() AnnoteOption { + return func(config *AnnotateOptions) { + enabled := true + config.httpStreamRequestBody = &enabled + } +} + +func WithGracefulShutdownSeconds(gracefulShutdownSeconds int) AnnoteOption { + return func(config *AnnotateOptions) { + config.gracefulShutdownSeconds = &gracefulShutdownSeconds + } +} diff --git a/pkg/kubernetes/annotator_test.go b/pkg/kubernetes/annotator_test.go new file mode 100644 index 000000000..77b4068f4 --- /dev/null +++ b/pkg/kubernetes/annotator_test.go @@ -0,0 +1,451 @@ +package kubernetes + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "os" + "path" + "sort" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +const ( + annotatorTestDataDir = "testdata/annotator" +) + +var ( + podInDir = path.Join(annotatorTestDataDir, "pod/in") + podOutDir = path.Join(annotatorTestDataDir, "pod/out") + deploymentInDir = path.Join(annotatorTestDataDir, "deployment/in") + deploymentOutDir = path.Join(annotatorTestDataDir, "deployment/out") + multiInDir = path.Join(annotatorTestDataDir, "multi/in") + multiOutDir = path.Join(annotatorTestDataDir, "multi/out") + listInDir = path.Join(annotatorTestDataDir, "list/in") + listOutDir = path.Join(annotatorTestDataDir, "list/out") +) + +type annotation struct { + targetResource string + targetNamespace string + optionFactory func() AnnotateOptions +} + +// nolint +func TestAnnotate(t *testing.T) { + // Helper function used to order test documents. + sortDocs := func(docs []string) { + sort.Slice(docs, func(i, j int) bool { + if len(docs[i]) == len(docs[j]) { + panic("Cannot sort docs with the same length, please ensure tests docs are a unique length.") + } + return len(docs[i]) < len(docs[j]) + }) + } + + configs := []struct { + testID string + annotations []annotation + inputFilePath string + expectedFilePath string + printOutput bool + }{ + { + testID: "single targeted annotation of a pod (config 1)", + annotations: []annotation{ + { + targetResource: "mypod", + optionFactory: func() AnnotateOptions { + return NewAnnotateOptions( + WithAppID("test-app"), + ) + }, + }, + }, + inputFilePath: path.Join(podInDir, "raw.yml"), + expectedFilePath: path.Join(podOutDir, "config_1.yml"), + // printOutput: true, // Uncomment to debug. + }, + { + testID: "single targeted annotation of a pod (config 2)", + annotations: []annotation{ + { + targetResource: "mypod", + optionFactory: func() AnnotateOptions { + return NewAnnotateOptions( + WithAppID("test-app"), + WithProfileEnabled(), + WithLogLevel("info"), + WithDaprImage("custom-image"), + ) + }, + }, + }, + inputFilePath: path.Join(podInDir, "raw.yml"), + expectedFilePath: path.Join(podOutDir, "config_2.yml"), + // printOutput: true, // Uncomment to debug. + }, + { + testID: "single targeted annotation of a pod without an app id in default namespace (config 3)", + annotations: []annotation{ + { + targetResource: "mypod", + optionFactory: func() AnnotateOptions { + return NewAnnotateOptions() + }, + }, + }, + inputFilePath: path.Join(podInDir, "raw.yml"), + expectedFilePath: path.Join(podOutDir, "config_3.yml"), + // printOutput: true, // Uncomment to debug. + }, + { + testID: "single targeted annotation of a pod without an app id in a namespace (config 4)", + annotations: []annotation{ + { + targetResource: "mypod", + optionFactory: func() AnnotateOptions { + return NewAnnotateOptions() + }, + }, + }, + inputFilePath: path.Join(podInDir, "namespace.yml"), + expectedFilePath: path.Join(podOutDir, "config_4.yml"), + // printOutput: true, // Uncomment to debug. + }, + { + testID: "single targeted annotation of a deployment (config 1)", + annotations: []annotation{ + { + targetResource: "nodeapp", + optionFactory: func() AnnotateOptions { + return NewAnnotateOptions( + WithAppID("nodeapp"), + WithAppPort(3000), + ) + }, + }, + }, + inputFilePath: path.Join(deploymentInDir, "raw.yml"), + expectedFilePath: path.Join(deploymentOutDir, "config_1.yml"), + // printOutput: true, // Uncomment to debug. + }, + { + testID: "partial annotation of a deployment (config 2)", + annotations: []annotation{ + { + targetResource: "nodeapp", + optionFactory: func() AnnotateOptions { + return NewAnnotateOptions( + WithAppID("nodeapp"), + WithAppPort(3000), + ) + }, + }, + }, + inputFilePath: path.Join(deploymentInDir, "partial.yml"), + expectedFilePath: path.Join(deploymentOutDir, "config_1.yml"), + // printOutput: true, // Uncomment to debug. + }, + { + testID: "single targeted annotation of multiple resources (config 1)", + annotations: []annotation{ + { + targetResource: "divideapp", + optionFactory: func() AnnotateOptions { + return NewAnnotateOptions( + WithAppID("divideapp"), + WithAppPort(4000), + WithConfig("appconfig"), + ) + }, + }, + }, + inputFilePath: path.Join(multiInDir, "raw.yml"), + expectedFilePath: path.Join(multiOutDir, "config_1.yml"), + // printOutput: true, // Uncomment to debug. + }, + { + testID: "multiple targeted annotations of multiple resources (config 2)", + annotations: []annotation{ + { + targetResource: "subtractapp", + optionFactory: func() AnnotateOptions { + return NewAnnotateOptions( + WithAppID("subtractapp"), + WithAppPort(80), + WithConfig("appconfig"), + ) + }, + }, + { + targetResource: "addapp", + optionFactory: func() AnnotateOptions { + return NewAnnotateOptions( + WithAppID("addapp"), + WithAppPort(6000), + WithConfig("appconfig"), + ) + }, + }, + { + targetResource: "multiplyapp", + optionFactory: func() AnnotateOptions { + return NewAnnotateOptions( + WithAppID("multiplyapp"), + WithAppPort(5000), + WithConfig("appconfig"), + ) + }, + }, + { + targetResource: "divideapp", + optionFactory: func() AnnotateOptions { + return NewAnnotateOptions( + WithAppID("divideapp"), + WithAppPort(4000), + WithConfig("appconfig"), + ) + }, + }, + { + targetResource: "calculator-front-end", + optionFactory: func() AnnotateOptions { + return NewAnnotateOptions( + WithAppID("calculator-front-end"), + WithAppPort(8080), + WithConfig("appconfig"), + ) + }, + }, + }, + inputFilePath: path.Join(multiInDir, "raw.yml"), + expectedFilePath: path.Join(multiOutDir, "config_2.yml"), + // printOutput: true, // Uncomment to debug. + }, + { + testID: "single untargeted annotations of multiple resources (config 3)", + annotations: []annotation{ + { + optionFactory: func() AnnotateOptions { + return NewAnnotateOptions( + WithAppID("subtractapp"), + WithAppPort(80), + WithConfig("appconfig"), + ) + }, + }, + }, + inputFilePath: path.Join(multiInDir, "raw.yml"), + expectedFilePath: path.Join(multiOutDir, "config_3.yml"), + // printOutput: true, // Uncomment to debug. + }, + { + testID: "single targeted annotations of multiple resources with a namespace (config 4)", + annotations: []annotation{ + { + targetResource: "subtractapp", + targetNamespace: "test1", + optionFactory: func() AnnotateOptions { + return NewAnnotateOptions( + WithAppID("subtractapp"), + ) + }, + }, + }, + inputFilePath: path.Join(multiInDir, "namespace.yml"), + expectedFilePath: path.Join(multiOutDir, "config_4.yml"), + // printOutput: true, // Uncomment to debug. + }, + { + testID: "single untargeted annotations of a list config", + annotations: []annotation{ + { + optionFactory: func() AnnotateOptions { + return NewAnnotateOptions( + WithAppID("nodeapp"), + WithAppPort(3000), + ) + }, + }, + }, + inputFilePath: path.Join(listInDir, "raw.yml"), + expectedFilePath: path.Join(listOutDir, "config_1.yml"), + // printOutput: true, // Uncomment to debug. + }, + } + + for _, tt := range configs { + t.Run(tt.testID, func(t *testing.T) { + inputFile, err := os.Open(tt.inputFilePath) + assert.NoError(t, err) + + defer func() { + err = inputFile.Close() + assert.NoError(t, err) + }() + + // Iterate through all the annotations and pipe them together. + var out bytes.Buffer + in := []io.Reader{inputFile} + for i, annotation := range tt.annotations { + annotator := NewK8sAnnotator(K8sAnnotatorConfig{ + TargetResource: &annotation.targetResource, + TargetNamespace: &annotation.targetNamespace, + }) + annotateOptions := annotation.optionFactory() + + out.Reset() + err = annotator.Annotate(in, &out, annotateOptions) + assert.NoError(t, err) + + // if it isn't the last annotation then set input to this annotation output + // to support testing chained resources. + if i != len(tt.annotations)-1 { + outReader := strings.NewReader(out.String()) + in = []io.Reader{outReader} + } + } + + // Split the multi-document string into individual documents for comparison. + outString := out.String() + outDocs := strings.Split(outString, "---") + + expected, err := ioutil.ReadFile(tt.expectedFilePath) + assert.NoError(t, err) + + expectedString := string(expected) + expectedDocs := strings.Split(expectedString, "---") + + // We must sort the documents to ensure we are comparing the correct documents. + // The content of the documents should be equivalent but it will not be the same + // as the order of keys are not being preserved. Therefore, we sort on the content + // length instead. This isn't perfect as additional character may be included but + // as long as we have enough spread between the documents we should be ok to use this + // to get an order. sortDocs will panic if it tries to compare content that is the + // same length as we would lose ordering but invalid orders are still possible. + sortDocs(outDocs) + sortDocs(expectedDocs) + assert.Equal(t, len(expectedDocs), len(outDocs)) + + for i := range expectedDocs { + if tt.printOutput { + t.Logf(outDocs[i]) + } + assert.YAMLEq(t, expectedDocs[i], outDocs[i]) + } + }) + } +} + +func TestGetDaprAnnotations(t *testing.T) { + t.Run("get dapr annotations", func(t *testing.T) { + appID := "test-app" + metricsPort := 9090 + apiTokenSecret := "test-api-token-secret" // #nosec + appTokenSecret := "test-app-token-secret" // #nosec + appMaxConcurrency := 2 + appPort := 8080 + appProtocol := "http" + cpuLimit := "0.5" + memoryLimit := "512Mi" + cpuRequest := "0.1" + memoryRequest := "256Mi" + config := "appconfig" + debugPort := 9091 + env := "key=value key1=value1" + listenAddresses := "0.0.0.0" + daprImage := "test-iamge" + maxRequestBodySize := 8 + readBufferSize := 4 + livenessProbeDelay := 10 + livenessProbePeriod := 20 + livenessProbeThreshold := 3 + livenessProbeTimeout := 30 + readinessProbeDelay := 40 + readinessProbePeriod := 50 + readinessProbeThreshold := 6 + readinessProbeTimeout := 60 + logLevel := "debug" + gracefulShutdownSeconds := 10 + + opts := NewAnnotateOptions( + WithAppID(appID), + WithMetricsEnabled(), + WithMetricsPort(metricsPort), + WithAPITokenSecret(apiTokenSecret), + WithAppTokenSecret(appTokenSecret), + WithAppMaxConcurrency(appMaxConcurrency), + WithAppPort(appPort), + WithAppProtocol(appProtocol), + WithAppSSL(), + WithCPULimit(cpuLimit), + WithMemoryLimit(memoryLimit), + WithCPURequest(cpuRequest), + WithMemoryRequest(memoryRequest), + WithConfig(config), + WithDebugEnabled(), + WithDebugPort(debugPort), + WithEnv(env), + WithLogAsJSON(), + WithListenAddresses(listenAddresses), + WithDaprImage(daprImage), + WithProfileEnabled(), + WithMaxRequestBodySize(maxRequestBodySize), + WithReadBufferSize(readBufferSize), + WithReadinessProbeDelay(readinessProbeDelay), + WithReadinessProbePeriod(readinessProbePeriod), + WithReadinessProbeThreshold(readinessProbeThreshold), + WithReadinessProbeTimeout(readinessProbeTimeout), + WithLivenessProbeDelay(livenessProbeDelay), + WithLivenessProbePeriod(livenessProbePeriod), + WithLivenessProbeThreshold(livenessProbeThreshold), + WithLivenessProbeTimeout(livenessProbeTimeout), + WithLogLevel(logLevel), + WithHTTPStreamRequestBody(), + WithGracefulShutdownSeconds(gracefulShutdownSeconds), + ) + + annotations := getDaprAnnotations(&opts) + + assert.Equal(t, "true", annotations[daprEnabledKey]) + assert.Equal(t, appID, annotations[daprAppIDKey]) + assert.Equal(t, fmt.Sprintf("%d", appPort), annotations[daprAppPortKey]) + assert.Equal(t, config, annotations[daprConfigKey]) + assert.Equal(t, appProtocol, annotations[daprAppProtocolKey]) + assert.Equal(t, "true", annotations[daprEnableProfilingKey]) + assert.Equal(t, logLevel, annotations[daprLogLevelKey]) + assert.Equal(t, apiTokenSecret, annotations[daprAPITokenSecretKey]) + assert.Equal(t, appTokenSecret, annotations[daprAppTokenSecretKey]) + assert.Equal(t, "true", annotations[daprLogAsJSONKey]) + assert.Equal(t, fmt.Sprintf("%d", appMaxConcurrency), annotations[daprAppMaxConcurrencyKey]) + assert.Equal(t, "true", annotations[daprEnableMetricsKey]) + assert.Equal(t, fmt.Sprintf("%d", metricsPort), annotations[daprMetricsPortKey]) + assert.Equal(t, "true", annotations[daprEnableDebugKey]) + assert.Equal(t, fmt.Sprintf("%d", debugPort), annotations[daprDebugPortKey]) + assert.Equal(t, env, annotations[daprEnvKey]) + assert.Equal(t, cpuLimit, annotations[daprCPULimitKey]) + assert.Equal(t, memoryLimit, annotations[daprMemoryLimitKey]) + assert.Equal(t, cpuRequest, annotations[daprCPURequestKey]) + assert.Equal(t, memoryRequest, annotations[daprMemoryRequestKey]) + assert.Equal(t, listenAddresses, annotations[daprListenAddressesKey]) + assert.Equal(t, fmt.Sprintf("%d", livenessProbeDelay), annotations[daprLivenessProbeDelayKey]) + assert.Equal(t, fmt.Sprintf("%d", livenessProbeTimeout), annotations[daprLivenessProbeTimeoutKey]) + assert.Equal(t, fmt.Sprintf("%d", livenessProbePeriod), annotations[daprLivenessProbePeriodKey]) + assert.Equal(t, fmt.Sprintf("%d", livenessProbeThreshold), annotations[daprLivenessProbeThresholdKey]) + assert.Equal(t, fmt.Sprintf("%d", readinessProbeDelay), annotations[daprReadinessProbeDelayKey]) + assert.Equal(t, fmt.Sprintf("%d", readinessProbeTimeout), annotations[daprReadinessProbeTimeoutKey]) + assert.Equal(t, fmt.Sprintf("%d", readinessProbePeriod), annotations[daprReadinessProbePeriodKey]) + assert.Equal(t, fmt.Sprintf("%d", readinessProbeThreshold), annotations[daprReadinessProbeThresholdKey]) + assert.Equal(t, daprImage, annotations[daprImageKey]) + assert.Equal(t, "true", annotations[daprAppSSLKey]) + assert.Equal(t, fmt.Sprintf("%d", maxRequestBodySize), annotations[daprMaxRequestBodySizeKey]) + assert.Equal(t, fmt.Sprintf("%d", readBufferSize), annotations[daprReadBufferSizeKey]) + assert.Equal(t, "true", annotations[daprHTTPStreamRequestBodyKey]) + assert.Equal(t, fmt.Sprintf("%d", gracefulShutdownSeconds), annotations[daprGracefulShutdownSecondsKey]) + }) +} diff --git a/pkg/kubernetes/testdata/annotator/deployment/in/partial.yml b/pkg/kubernetes/testdata/annotator/deployment/in/partial.yml new file mode 100644 index 000000000..99f51dc32 --- /dev/null +++ b/pkg/kubernetes/testdata/annotator/deployment/in/partial.yml @@ -0,0 +1,25 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nodeapp + labels: + app: node +spec: + replicas: 1 + selector: + matchLabels: + app: node + template: + metadata: + labels: + app: node + annotations: + dapr.io/enabled: "false" + dapr.io/app-id: "node-app" + spec: + containers: + - name: node + image: dapriosamples/hello-k8s-node:latest + ports: + - containerPort: 3000 + imagePullPolicy: Always \ No newline at end of file diff --git a/pkg/kubernetes/testdata/annotator/deployment/in/raw.yml b/pkg/kubernetes/testdata/annotator/deployment/in/raw.yml new file mode 100644 index 000000000..ce38cd960 --- /dev/null +++ b/pkg/kubernetes/testdata/annotator/deployment/in/raw.yml @@ -0,0 +1,22 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nodeapp + labels: + app: node +spec: + replicas: 1 + selector: + matchLabels: + app: node + template: + metadata: + labels: + app: node + spec: + containers: + - name: node + image: dapriosamples/hello-k8s-node:latest + ports: + - containerPort: 3000 + imagePullPolicy: Always \ No newline at end of file diff --git a/pkg/kubernetes/testdata/annotator/deployment/out/config_1.yml b/pkg/kubernetes/testdata/annotator/deployment/out/config_1.yml new file mode 100644 index 000000000..d5dc37cdc --- /dev/null +++ b/pkg/kubernetes/testdata/annotator/deployment/out/config_1.yml @@ -0,0 +1,26 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nodeapp + labels: + app: node +spec: + replicas: 1 + selector: + matchLabels: + app: node + template: + metadata: + labels: + app: node + annotations: + dapr.io/enabled: "true" + dapr.io/app-id: "nodeapp" + dapr.io/app-port: "3000" + spec: + containers: + - name: node + image: dapriosamples/hello-k8s-node:latest + ports: + - containerPort: 3000 + imagePullPolicy: Always \ No newline at end of file diff --git a/pkg/kubernetes/testdata/annotator/list/in/raw.yml b/pkg/kubernetes/testdata/annotator/list/in/raw.yml new file mode 100644 index 000000000..46fee7600 --- /dev/null +++ b/pkg/kubernetes/testdata/annotator/list/in/raw.yml @@ -0,0 +1,46 @@ +apiVersion: v1 +items: +- apiVersion: apps/v1 + kind: Deployment + metadata: + annotations: + deployment.kubernetes.io/revision: "1" + creationTimestamp: "2022-01-14T17:11:56Z" + generation: 1 + labels: + app: node + spec: + progressDeadlineSeconds: 600 + replicas: 1 + revisionHistoryLimit: 10 + selector: + matchLabels: + app: node + strategy: + rollingUpdate: + maxSurge: 25% + maxUnavailable: 25% + type: RollingUpdate + template: + metadata: + creationTimestamp: null + labels: + app: node + spec: + containers: + - image: dapriosamples/hello-k8s-node:latest + imagePullPolicy: Always + name: node + ports: + - containerPort: 3000 + protocol: TCP + resources: {} + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + dnsPolicy: ClusterFirst + restartPolicy: Always + schedulerName: default-scheduler + securityContext: {} + terminationGracePeriodSeconds: 30 +kind: List +metadata: {} \ No newline at end of file diff --git a/pkg/kubernetes/testdata/annotator/list/out/config_1.yml b/pkg/kubernetes/testdata/annotator/list/out/config_1.yml new file mode 100644 index 000000000..c382bcf97 --- /dev/null +++ b/pkg/kubernetes/testdata/annotator/list/out/config_1.yml @@ -0,0 +1,50 @@ +apiVersion: v1 +items: +- apiVersion: apps/v1 + kind: Deployment + metadata: + annotations: + deployment.kubernetes.io/revision: "1" + creationTimestamp: "2022-01-14T17:11:56Z" + generation: 1 + labels: + app: node + spec: + progressDeadlineSeconds: 600 + replicas: 1 + revisionHistoryLimit: 10 + selector: + matchLabels: + app: node + strategy: + rollingUpdate: + maxSurge: 25% + maxUnavailable: 25% + type: RollingUpdate + template: + metadata: + creationTimestamp: null + labels: + app: node + annotations: + dapr.io/enabled: "true" + dapr.io/app-id: "nodeapp" + dapr.io/app-port: "3000" + spec: + containers: + - image: dapriosamples/hello-k8s-node:latest + imagePullPolicy: Always + name: node + ports: + - containerPort: 3000 + protocol: TCP + resources: {} + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + dnsPolicy: ClusterFirst + restartPolicy: Always + schedulerName: default-scheduler + securityContext: {} + terminationGracePeriodSeconds: 30 +kind: List +metadata: {} \ No newline at end of file diff --git a/pkg/kubernetes/testdata/annotator/multi/in/namespace.yml b/pkg/kubernetes/testdata/annotator/multi/in/namespace.yml new file mode 100644 index 000000000..1c3f2eb41 --- /dev/null +++ b/pkg/kubernetes/testdata/annotator/multi/in/namespace.yml @@ -0,0 +1,47 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: subtractapp + namespace: test + labels: + app: subtract +spec: + replicas: 1 + selector: + matchLabels: + app: subtract + template: + metadata: + labels: + app: subtract + spec: + containers: + - name: subtract + image: dapriosamples/distributed-calculator-csharp:latest + ports: + - containerPort: 80 + imagePullPolicy: Always +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: subtractapp + namespace: test1 + labels: + app: subtract +spec: + replicas: 1 + selector: + matchLabels: + app: subtract + template: + metadata: + labels: + app: subtract + spec: + containers: + - name: subtract + image: dapriosamples/distributed-calculator-csharp:latest + ports: + - containerPort: 80 + imagePullPolicy: Always \ No newline at end of file diff --git a/pkg/kubernetes/testdata/annotator/multi/in/raw.yml b/pkg/kubernetes/testdata/annotator/multi/in/raw.yml new file mode 100644 index 000000000..3a310eddc --- /dev/null +++ b/pkg/kubernetes/testdata/annotator/multi/in/raw.yml @@ -0,0 +1,162 @@ +apiVersion: dapr.io/v1alpha1 +kind: Configuration +metadata: + name: appconfig +spec: + tracing: + samplingRate: "1" + zipkin: + endpointAddress: "http://zipkin.default.svc.cluster.local:9411/api/v2/spans" +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: subtractapp + labels: + app: subtract +spec: + replicas: 1 + selector: + matchLabels: + app: subtract + template: + metadata: + labels: + app: subtract + spec: + containers: + - name: subtract + image: dapriosamples/distributed-calculator-csharp:latest + ports: + - containerPort: 80 + imagePullPolicy: Always +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: addapp + labels: + app: add +spec: + replicas: 1 + selector: + matchLabels: + app: add + template: + metadata: + labels: + app: add + spec: + containers: + - name: add + image: dapriosamples/distributed-calculator-go:latest + ports: + - containerPort: 6000 + imagePullPolicy: Always +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: divideapp + labels: + app: divide +spec: + replicas: 1 + selector: + matchLabels: + app: divide + template: + metadata: + labels: + app: divide + spec: + containers: + - name: divide + image: dapriosamples/distributed-calculator-node:latest + ports: + - containerPort: 4000 + imagePullPolicy: Always +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: multiplyapp + labels: + app: multiply +spec: + replicas: 1 + selector: + matchLabels: + app: multiply + template: + metadata: + labels: + app: multiply + spec: + containers: + - name: multiply + image: dapriosamples/distributed-calculator-python:latest + ports: + - containerPort: 5000 + imagePullPolicy: Always +--- +kind: Service +apiVersion: v1 +metadata: + name: calculator-front-end + labels: + app: calculator-front-end +spec: + selector: + app: calculator-front-end + ports: + - protocol: TCP + port: 80 + targetPort: 8080 + type: LoadBalancer + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: calculator-front-end + labels: + app: calculator-front-end +spec: + replicas: 1 + selector: + matchLabels: + app: calculator-front-end + template: + metadata: + labels: + app: calculator-front-end + spec: + containers: + - name: calculator-front-end + image: dapriosamples/distributed-calculator-react-calculator:latest + ports: + - containerPort: 8080 + imagePullPolicy: Always +--- +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: statestore +spec: + type: state.redis + version: v1 + metadata: + # These settings will work out of the box if you use `helm install + # bitnami/redis`. If you have your own setup, replace + # `redis-master:6379` with your own Redis master address, and the + # Redis password with your own Secret's name. For more information, + # see https://docs.dapr.io/operations/components/component-secrets . + - name: redisHost + value: redis-master:6379 + - name: redisPassword + secretKeyRef: + name: redis + key: redis-password +auth: + secretStore: kubernetes \ No newline at end of file diff --git a/pkg/kubernetes/testdata/annotator/multi/out/config_1.yml b/pkg/kubernetes/testdata/annotator/multi/out/config_1.yml new file mode 100644 index 000000000..ec847633c --- /dev/null +++ b/pkg/kubernetes/testdata/annotator/multi/out/config_1.yml @@ -0,0 +1,166 @@ +apiVersion: dapr.io/v1alpha1 +kind: Configuration +metadata: + name: appconfig +spec: + tracing: + samplingRate: "1" + zipkin: + endpointAddress: "http://zipkin.default.svc.cluster.local:9411/api/v2/spans" +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: subtractapp + labels: + app: subtract +spec: + replicas: 1 + selector: + matchLabels: + app: subtract + template: + metadata: + labels: + app: subtract + spec: + containers: + - name: subtract + image: dapriosamples/distributed-calculator-csharp:latest + ports: + - containerPort: 80 + imagePullPolicy: Always +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: addapp + labels: + app: add +spec: + replicas: 1 + selector: + matchLabels: + app: add + template: + metadata: + labels: + app: add + spec: + containers: + - name: add + image: dapriosamples/distributed-calculator-go:latest + ports: + - containerPort: 6000 + imagePullPolicy: Always +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: divideapp + labels: + app: divide +spec: + replicas: 1 + selector: + matchLabels: + app: divide + template: + metadata: + labels: + app: divide + annotations: + dapr.io/enabled: "true" + dapr.io/app-id: "divideapp" + dapr.io/app-port: "4000" + dapr.io/config: "appconfig" + spec: + containers: + - name: divide + image: dapriosamples/distributed-calculator-node:latest + ports: + - containerPort: 4000 + imagePullPolicy: Always +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: multiplyapp + labels: + app: multiply +spec: + replicas: 1 + selector: + matchLabels: + app: multiply + template: + metadata: + labels: + app: multiply + spec: + containers: + - name: multiply + image: dapriosamples/distributed-calculator-python:latest + ports: + - containerPort: 5000 + imagePullPolicy: Always +--- +kind: Service +apiVersion: v1 +metadata: + name: calculator-front-end + labels: + app: calculator-front-end +spec: + selector: + app: calculator-front-end + ports: + - protocol: TCP + port: 80 + targetPort: 8080 + type: LoadBalancer +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: calculator-front-end + labels: + app: calculator-front-end +spec: + replicas: 1 + selector: + matchLabels: + app: calculator-front-end + template: + metadata: + labels: + app: calculator-front-end + spec: + containers: + - name: calculator-front-end + image: dapriosamples/distributed-calculator-react-calculator:latest + ports: + - containerPort: 8080 + imagePullPolicy: Always +--- +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: statestore +spec: + type: state.redis + version: v1 + metadata: + # These settings will work out of the box if you use `helm install + # bitnami/redis`. If you have your own setup, replace + # `redis-master:6379` with your own Redis master address, and the + # Redis password with your own Secret's name. For more information, + # see https://docs.dapr.io/operations/components/component-secrets . + - name: redisHost + value: redis-master:6379 + - name: redisPassword + secretKeyRef: + name: redis + key: redis-password +auth: + secretStore: kubernetes \ No newline at end of file diff --git a/pkg/kubernetes/testdata/annotator/multi/out/config_2.yml b/pkg/kubernetes/testdata/annotator/multi/out/config_2.yml new file mode 100644 index 000000000..e721401f8 --- /dev/null +++ b/pkg/kubernetes/testdata/annotator/multi/out/config_2.yml @@ -0,0 +1,186 @@ +apiVersion: dapr.io/v1alpha1 +kind: Configuration +metadata: + name: appconfig +spec: + tracing: + samplingRate: "1" + zipkin: + endpointAddress: "http://zipkin.default.svc.cluster.local:9411/api/v2/spans" +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: subtractapp + labels: + app: subtract +spec: + replicas: 1 + selector: + matchLabels: + app: subtract + template: + metadata: + labels: + app: subtract + annotations: + dapr.io/enabled: "true" + dapr.io/app-id: "subtractapp" + dapr.io/app-port: "80" + dapr.io/config: "appconfig" + spec: + containers: + - name: subtract + image: dapriosamples/distributed-calculator-csharp:latest + ports: + - containerPort: 80 + imagePullPolicy: Always +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: addapp + labels: + app: add +spec: + replicas: 1 + selector: + matchLabels: + app: add + template: + metadata: + labels: + app: add + annotations: + dapr.io/enabled: "true" + dapr.io/app-id: "addapp" + dapr.io/app-port: "6000" + dapr.io/config: "appconfig" + spec: + containers: + - name: add + image: dapriosamples/distributed-calculator-go:latest + ports: + - containerPort: 6000 + imagePullPolicy: Always +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: divideapp + labels: + app: divide +spec: + replicas: 1 + selector: + matchLabels: + app: divide + template: + metadata: + labels: + app: divide + annotations: + dapr.io/enabled: "true" + dapr.io/app-id: "divideapp" + dapr.io/app-port: "4000" + dapr.io/config: "appconfig" + spec: + containers: + - name: divide + image: dapriosamples/distributed-calculator-node:latest + ports: + - containerPort: 4000 + imagePullPolicy: Always +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: multiplyapp + labels: + app: multiply +spec: + replicas: 1 + selector: + matchLabels: + app: multiply + template: + metadata: + labels: + app: multiply + annotations: + dapr.io/enabled: "true" + dapr.io/app-id: "multiplyapp" + dapr.io/app-port: "5000" + dapr.io/config: "appconfig" + spec: + containers: + - name: multiply + image: dapriosamples/distributed-calculator-python:latest + ports: + - containerPort: 5000 + imagePullPolicy: Always +--- +kind: Service +apiVersion: v1 +metadata: + name: calculator-front-end + labels: + app: calculator-front-end +spec: + selector: + app: calculator-front-end + ports: + - protocol: TCP + port: 80 + targetPort: 8080 + type: LoadBalancer +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: calculator-front-end + labels: + app: calculator-front-end +spec: + replicas: 1 + selector: + matchLabels: + app: calculator-front-end + template: + metadata: + labels: + app: calculator-front-end + annotations: + dapr.io/enabled: "true" + dapr.io/app-id: "calculator-front-end" + dapr.io/app-port: "8080" + dapr.io/config: "appconfig" + spec: + containers: + - name: calculator-front-end + image: dapriosamples/distributed-calculator-react-calculator:latest + ports: + - containerPort: 8080 + imagePullPolicy: Always +--- +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: statestore +spec: + type: state.redis + version: v1 + metadata: + # These settings will work out of the box if you use `helm install + # bitnami/redis`. If you have your own setup, replace + # `redis-master:6379` with your own Redis master address, and the + # Redis password with your own Secret's name. For more information, + # see https://docs.dapr.io/operations/components/component-secrets . + - name: redisHost + value: redis-master:6379 + - name: redisPassword + secretKeyRef: + name: redis + key: redis-password +auth: + secretStore: kubernetes \ No newline at end of file diff --git a/pkg/kubernetes/testdata/annotator/multi/out/config_3.yml b/pkg/kubernetes/testdata/annotator/multi/out/config_3.yml new file mode 100644 index 000000000..36be9c631 --- /dev/null +++ b/pkg/kubernetes/testdata/annotator/multi/out/config_3.yml @@ -0,0 +1,166 @@ +apiVersion: dapr.io/v1alpha1 +kind: Configuration +metadata: + name: appconfig +spec: + tracing: + samplingRate: "1" + zipkin: + endpointAddress: "http://zipkin.default.svc.cluster.local:9411/api/v2/spans" +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: subtractapp + labels: + app: subtract +spec: + replicas: 1 + selector: + matchLabels: + app: subtract + template: + metadata: + labels: + app: subtract + annotations: + dapr.io/enabled: "true" + dapr.io/app-id: "subtractapp" + dapr.io/app-port: "80" + dapr.io/config: "appconfig" + spec: + containers: + - name: subtract + image: dapriosamples/distributed-calculator-csharp:latest + ports: + - containerPort: 80 + imagePullPolicy: Always +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: addapp + labels: + app: add +spec: + replicas: 1 + selector: + matchLabels: + app: add + template: + metadata: + labels: + app: add + spec: + containers: + - name: add + image: dapriosamples/distributed-calculator-go:latest + ports: + - containerPort: 6000 + imagePullPolicy: Always +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: divideapp + labels: + app: divide +spec: + replicas: 1 + selector: + matchLabels: + app: divide + template: + metadata: + labels: + app: divide + spec: + containers: + - name: divide + image: dapriosamples/distributed-calculator-node:latest + ports: + - containerPort: 4000 + imagePullPolicy: Always +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: multiplyapp + labels: + app: multiply +spec: + replicas: 1 + selector: + matchLabels: + app: multiply + template: + metadata: + labels: + app: multiply + spec: + containers: + - name: multiply + image: dapriosamples/distributed-calculator-python:latest + ports: + - containerPort: 5000 + imagePullPolicy: Always +--- +kind: Service +apiVersion: v1 +metadata: + name: calculator-front-end + labels: + app: calculator-front-end +spec: + selector: + app: calculator-front-end + ports: + - protocol: TCP + port: 80 + targetPort: 8080 + type: LoadBalancer +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: calculator-front-end + labels: + app: calculator-front-end +spec: + replicas: 1 + selector: + matchLabels: + app: calculator-front-end + template: + metadata: + labels: + app: calculator-front-end + spec: + containers: + - name: calculator-front-end + image: dapriosamples/distributed-calculator-react-calculator:latest + ports: + - containerPort: 8080 + imagePullPolicy: Always +--- +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: statestore +spec: + type: state.redis + version: v1 + metadata: + # These settings will work out of the box if you use `helm install + # bitnami/redis`. If you have your own setup, replace + # `redis-master:6379` with your own Redis master address, and the + # Redis password with your own Secret's name. For more information, + # see https://docs.dapr.io/operations/components/component-secrets . + - name: redisHost + value: redis-master:6379 + - name: redisPassword + secretKeyRef: + name: redis + key: redis-password +auth: + secretStore: kubernetes \ No newline at end of file diff --git a/pkg/kubernetes/testdata/annotator/multi/out/config_4.yml b/pkg/kubernetes/testdata/annotator/multi/out/config_4.yml new file mode 100644 index 000000000..6b8363e63 --- /dev/null +++ b/pkg/kubernetes/testdata/annotator/multi/out/config_4.yml @@ -0,0 +1,50 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: subtractapp + namespace: test + labels: + app: subtract +spec: + replicas: 1 + selector: + matchLabels: + app: subtract + template: + metadata: + labels: + app: subtract + spec: + containers: + - name: subtract + image: dapriosamples/distributed-calculator-csharp:latest + ports: + - containerPort: 80 + imagePullPolicy: Always +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: subtractapp + namespace: test1 + labels: + app: subtract +spec: + replicas: 1 + selector: + matchLabels: + app: subtract + template: + metadata: + labels: + app: subtract + annotations: + dapr.io/enabled: "true" + dapr.io/app-id: "subtractapp" + spec: + containers: + - name: subtract + image: dapriosamples/distributed-calculator-csharp:latest + ports: + - containerPort: 80 + imagePullPolicy: Always \ No newline at end of file diff --git a/pkg/kubernetes/testdata/annotator/pod/in/namespace.yml b/pkg/kubernetes/testdata/annotator/pod/in/namespace.yml new file mode 100644 index 000000000..d87609822 --- /dev/null +++ b/pkg/kubernetes/testdata/annotator/pod/in/namespace.yml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Pod +metadata: + name: mypod + namespace: test + labels: + name: nginx +spec: + containers: + - name: nginx + image: nginx + ports: + - containerPort: 80 \ No newline at end of file diff --git a/pkg/kubernetes/testdata/annotator/pod/in/raw.yml b/pkg/kubernetes/testdata/annotator/pod/in/raw.yml new file mode 100644 index 000000000..4b4b59188 --- /dev/null +++ b/pkg/kubernetes/testdata/annotator/pod/in/raw.yml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Pod +metadata: + name: mypod + labels: + name: nginx +spec: + containers: + - name: nginx + image: nginx + ports: + - containerPort: 80 \ No newline at end of file diff --git a/pkg/kubernetes/testdata/annotator/pod/out/config_1.yml b/pkg/kubernetes/testdata/annotator/pod/out/config_1.yml new file mode 100644 index 000000000..f8e61565d --- /dev/null +++ b/pkg/kubernetes/testdata/annotator/pod/out/config_1.yml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: mypod + labels: + name: nginx + annotations: + dapr.io/enabled: "true" + dapr.io/app-id: "test-app" +spec: + containers: + - name: nginx + image: nginx + ports: + - containerPort: 80 \ No newline at end of file diff --git a/pkg/kubernetes/testdata/annotator/pod/out/config_2.yml b/pkg/kubernetes/testdata/annotator/pod/out/config_2.yml new file mode 100644 index 000000000..1c6a4b45e --- /dev/null +++ b/pkg/kubernetes/testdata/annotator/pod/out/config_2.yml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: Pod +metadata: + name: mypod + labels: + name: nginx + annotations: + dapr.io/enabled: "true" + dapr.io/app-id: "test-app" + dapr.io/enable-profiling: "true" + dapr.io/log-level: "info" + dapr.io/sidecar-image: "custom-image" +spec: + containers: + - name: nginx + image: nginx + ports: + - containerPort: 80 \ No newline at end of file diff --git a/pkg/kubernetes/testdata/annotator/pod/out/config_3.yml b/pkg/kubernetes/testdata/annotator/pod/out/config_3.yml new file mode 100644 index 000000000..0dd6cb03c --- /dev/null +++ b/pkg/kubernetes/testdata/annotator/pod/out/config_3.yml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: mypod + labels: + name: nginx + annotations: + dapr.io/enabled: "true" + dapr.io/app-id: "default-pod-mypod" +spec: + containers: + - name: nginx + image: nginx + ports: + - containerPort: 80 \ No newline at end of file diff --git a/pkg/kubernetes/testdata/annotator/pod/out/config_4.yml b/pkg/kubernetes/testdata/annotator/pod/out/config_4.yml new file mode 100644 index 000000000..6f4ef800b --- /dev/null +++ b/pkg/kubernetes/testdata/annotator/pod/out/config_4.yml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Pod +metadata: + name: mypod + namespace: test + labels: + name: nginx + annotations: + dapr.io/enabled: "true" + dapr.io/app-id: "test-pod-mypod" +spec: + containers: + - name: nginx + image: nginx + ports: + - containerPort: 80 \ No newline at end of file