From debd3013196a1ef0bd2cfa9708ab3cc1876cf483 Mon Sep 17 00:00:00 2001 From: Vladislav Kuzmin Date: Thu, 22 Apr 2021 18:10:46 +0400 Subject: [PATCH] Add document filtering in KRM toolbox by GVK It allows to pass needed documents to KRM toolbox. Relates-To: #517 Change-Id: I9a8e78796b5fffab5ee44ad8cf73a88563589929 --- krm-functions/toolbox/README.md | 4 +- krm-functions/toolbox/main.go | 32 ++++- krm-functions/toolbox/main_test.go | 131 ++++++++++++++++++ .../function/phase-helpers/kustomization.yaml | 1 + .../pause_bmh/kubectl_pause_bmh.sh | 22 +++ .../pause_bmh/kustomization.yaml | 6 + manifests/phases/executors.yaml | 19 +++ manifests/phases/phases.yaml | 12 ++ .../deployment/33_cluster_move_target_node.sh | 12 +- 9 files changed, 227 insertions(+), 12 deletions(-) create mode 100644 manifests/function/phase-helpers/pause_bmh/kubectl_pause_bmh.sh create mode 100644 manifests/function/phase-helpers/pause_bmh/kustomization.yaml diff --git a/krm-functions/toolbox/README.md b/krm-functions/toolbox/README.md index 32b1b3503..7f55a4ea0 100644 --- a/krm-functions/toolbox/README.md +++ b/krm-functions/toolbox/README.md @@ -66,6 +66,8 @@ The toolbox image has pre-installed `sh` shell,`kubectl` and `calicoctl`. ## Input bundle usage The KRM function writes to filesystem input bundle specified in `documentEntryPoint` in phase declaration and imports the path to this bundle in `RENDERED_BUNDLE_PATH` environment variable. For example it can be used with `calicoctl` as `calicoctl apply -f $RENDERED_BUNDLE_PATH` +Documents can be filtered by group, version and kind. You need to set `RESOURCE_GROUP_FILTER`, `RESOURCE_VERSION_FILTER` and/or`RESOURCE_KIND_FILTER` in executor definition to enable filtering. ## Important notes -The script must write to STDOUT valid yaml or redirect output to STDERR otherwise phase will fail with `mapping values are not allowed in this context` +1. The script must write to STDOUT valid yaml or redirect output to STDERR otherwise phase will fail with `mapping values are not allowed in this context` +2. All shell scripts must begin with `set -xe`. This allows errors to be passed from the container to the airshipctl itself. Without this flags the container will never fail. diff --git a/krm-functions/toolbox/main.go b/krm-functions/toolbox/main.go index 74e602e03..401dcd6b1 100644 --- a/krm-functions/toolbox/main.go +++ b/krm-functions/toolbox/main.go @@ -28,16 +28,23 @@ import ( "sigs.k8s.io/kustomize/kyaml/kio" kyaml "sigs.k8s.io/kustomize/kyaml/yaml" + "opendev.org/airship/airshipctl/pkg/document/plugin/kyamlutils" "opendev.org/airship/airshipctl/pkg/log" ) const ( // EnvRenderedBundlePath will be passed to the script, it will contain path to the rendered bundle EnvRenderedBundlePath = "RENDERED_BUNDLE_PATH" - scriptPath = "script.sh" - scriptKey = "script" - bundleFile = "bundle.yaml" - workdir = "/tmp" + // ResourceGroupFilter used for filtering input bundle by document group + ResourceGroupFilter = "RESOURCE_GROUP_FILTER" + // ResourceVersionFilter used for filtering input bundle by document version + ResourceVersionFilter = "RESOURCE_VERSION_FILTER" + // ResourceKindFilter used for filtering input bundle by document kind + ResourceKindFilter = "RESOURCE_KIND_FILTER" + scriptPath = "script.sh" + scriptKey = "script" + bundleFile = "bundle.yaml" + workdir = "/tmp" ) func main() { @@ -137,6 +144,11 @@ func (c *ScriptRunner) writeBundle(path string, items []*kyaml.RNode) error { } defer f.Close() + items, err = c.FilterBundle(items) + if err != nil { + return err + } + pipeline := kio.Pipeline{ Outputs: []kio.Writer{ kio.ByteWriter{ @@ -150,3 +162,15 @@ func (c *ScriptRunner) writeBundle(path string, items []*kyaml.RNode) error { return pipeline.Execute() } + +// FilterBundle uses for filtering input documents +func (c *ScriptRunner) FilterBundle(items []*kyaml.RNode) ([]*kyaml.RNode, error) { + group := os.Getenv(ResourceGroupFilter) + version := os.Getenv(ResourceVersionFilter) + kind := os.Getenv(ResourceKindFilter) + log.Printf("Filtering input bundle by Group: %s, Version: %s, Kind: %s", + group, version, kind) + return kyamlutils.DocumentSelector{}. + ByGVK(group, version, kind). + Filter(items) +} diff --git a/krm-functions/toolbox/main_test.go b/krm-functions/toolbox/main_test.go index 660f66cc8..d761c82d6 100644 --- a/krm-functions/toolbox/main_test.go +++ b/krm-functions/toolbox/main_test.go @@ -17,6 +17,7 @@ package main import ( "bytes" "io/ioutil" + "os" "path/filepath" "testing" @@ -24,6 +25,7 @@ import ( "github.com/stretchr/testify/require" v1 "k8s.io/api/core/v1" "sigs.k8s.io/kustomize/kyaml/fn/framework" + "sigs.k8s.io/kustomize/kyaml/kio" "sigs.k8s.io/kustomize/kyaml/yaml" ) @@ -166,3 +168,132 @@ func TestCmdRunCleanup(t *testing.T) { }() assert.NoError(t, err) } + +func TestFilterBundle(t *testing.T) { + rawDocs := `--- +apiVersion: test/v1 +kind: Pod +metadata: + name: t1 +--- +apiVersion: test/v1 +kind: Secret +metadata: + name: t2 +--- +apiVersion: test/v1beta1 +kind: Deployment +metadata: + name: t3 +` + docs, err := (&kio.ByteReader{Reader: bytes.NewBufferString(rawDocs)}).Read() + require.NoError(t, err) + + testCases := []struct { + name string + inputDocs []*yaml.RNode + groupFilter string + versionFilter string + kindFilter string + errContains string + expectedDocs string + }{ + { + name: "Correct documents", + inputDocs: docs, + groupFilter: "test", + versionFilter: "v1", + kindFilter: "Pod", + errContains: "", + expectedDocs: `apiVersion: test/v1 +kind: Pod +metadata: + name: t1 +`, + }, + { + name: "Empty kind", + inputDocs: docs, + groupFilter: "test", + versionFilter: "v1beta1", + kindFilter: "", + errContains: "", + expectedDocs: `apiVersion: test/v1beta1 +kind: Deployment +metadata: + name: t3 +`, + }, + { + name: "Empty all filters", + inputDocs: docs, + groupFilter: "", + versionFilter: "", + kindFilter: "", + errContains: "", + expectedDocs: `apiVersion: test/v1 +kind: Pod +metadata: + name: t1 +--- +apiVersion: test/v1 +kind: Secret +metadata: + name: t2 +--- +apiVersion: test/v1beta1 +kind: Deployment +metadata: + name: t3 +`, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + cMap := &v1.ConfigMap{ + Data: map[string]string{ + dataKey: script, + }, + } + + input, err := yaml.Parse(inputString) + require.NoError(t, err) + + stderr := bytes.NewBuffer([]byte{}) + stdout := bytes.NewBuffer([]byte{}) + + os.Setenv(ResourceGroupFilter, tc.groupFilter) + defer os.Unsetenv(ResourceGroupFilter) + os.Setenv(ResourceVersionFilter, tc.versionFilter) + defer os.Unsetenv(ResourceVersionFilter) + os.Setenv(ResourceKindFilter, tc.kindFilter) + defer os.Unsetenv(ResourceKindFilter) + + cmd := &ScriptRunner{ + ScriptFile: targetFile, + WorkDir: dir, + DataKey: dataKey, + ErrStream: stderr, + OutStream: stdout, + ResourceList: &framework.ResourceList{Items: []*yaml.RNode{input}}, + ConfigMap: cMap, + RenderedBundleFile: bundlePath, + } + + filteredDocs, err := cmd.FilterBundle(tc.inputDocs) + if tc.errContains != "" { + require.Error(t, err) + assert.Contains(t, err.Error(), tc.errContains) + } else { + require.NoError(t, err) + + buf := &bytes.Buffer{} + err = kio.ByteWriter{Writer: buf}.Write(filteredDocs) + assert.Equal(t, tc.expectedDocs, buf.String()) + require.NoError(t, err) + } + }) + } +} diff --git a/manifests/function/phase-helpers/kustomization.yaml b/manifests/function/phase-helpers/kustomization.yaml index 2f7539d3b..cc20e86e4 100644 --- a/manifests/function/phase-helpers/kustomization.yaml +++ b/manifests/function/phase-helpers/kustomization.yaml @@ -5,3 +5,4 @@ resources: - wait_deploy - get_node - wait_pods +- pause_bmh diff --git a/manifests/function/phase-helpers/pause_bmh/kubectl_pause_bmh.sh b/manifests/function/phase-helpers/pause_bmh/kubectl_pause_bmh.sh new file mode 100644 index 000000000..181979077 --- /dev/null +++ b/manifests/function/phase-helpers/pause_bmh/kubectl_pause_bmh.sh @@ -0,0 +1,22 @@ +#!/bin/sh + +# 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. + +set -xe + +echo "Annotating BMH objects with a pause label in cluster with kubectl context ${KCTL_CONTEXT}" 1>&2 +kubectl annotate \ + --context $KCTL_CONTEXT \ + --namespace $CLUSTER_NAMESPACE \ + --overwrite \ + -f $RENDERED_BUNDLE_PATH baremetalhost.metal3.io/paused=true 1>&2 diff --git a/manifests/function/phase-helpers/pause_bmh/kustomization.yaml b/manifests/function/phase-helpers/pause_bmh/kustomization.yaml new file mode 100644 index 000000000..384b44c68 --- /dev/null +++ b/manifests/function/phase-helpers/pause_bmh/kustomization.yaml @@ -0,0 +1,6 @@ +configMapGenerator: +- name: kubectl-pause-bmh + options: + disableNameSuffixHash: true + files: + - script=kubectl_pause_bmh.sh diff --git a/manifests/phases/executors.yaml b/manifests/phases/executors.yaml index bb72f99a1..c45ec1e98 100644 --- a/manifests/phases/executors.yaml +++ b/manifests/phases/executors.yaml @@ -391,3 +391,22 @@ configRef: apiVersion: airshipit.org/v1alpha1 kind: KubevalOptions name: kubeval-options +--- +apiVersion: airshipit.org/v1alpha1 +kind: GenericContainer +metadata: + name: kubectl-pause-bmh + labels: + airshipit.org/deploy-k8s: "false" +spec: + image: quay.io/airshipit/toolbox:latest + hostNetwork: true + envVars: + - CLUSTER_NAMESPACE=default + - RESOURCE_GROUP_FILTER=metal3.io + - RESOURCE_VERSION_FILTER=v1alpha1 + - RESOURCE_KIND_FILTER=BareMetalHost +configRef: + kind: ConfigMap + name: kubectl-pause-bmh + apiVersion: v1 diff --git a/manifests/phases/phases.yaml b/manifests/phases/phases.yaml index 71c93bad1..72b253849 100644 --- a/manifests/phases/phases.yaml +++ b/manifests/phases/phases.yaml @@ -366,3 +366,15 @@ config: apiVersion: airshipit.org/v1alpha1 kind: GenericContainer name: kubectl-wait-pods +--- +apiVersion: airshipit.org/v1alpha1 +kind: Phase +metadata: + name: kubectl-pause-bmh + clusterName: ephemeral-cluster +config: + executorRef: + apiVersion: airshipit.org/v1alpha1 + kind: GenericContainer + name: kubectl-pause-bmh + documentEntryPoint: ephemeral/controlplane diff --git a/tools/deployment/33_cluster_move_target_node.sh b/tools/deployment/33_cluster_move_target_node.sh index 4f8a9a189..c58376949 100755 --- a/tools/deployment/33_cluster_move_target_node.sh +++ b/tools/deployment/33_cluster_move_target_node.sh @@ -17,16 +17,14 @@ set -xe #Default wait timeout is 3600 seconds export TIMEOUT=${TIMEOUT:-3600} export KUBECONFIG=${KUBECONFIG:-"$HOME/.airship/kubeconfig"} -export KUBECONFIG_EPHEMERAL_CONTEXT=${KUBECONFIG_EPHEMERAL_CONTEXT:-"ephemeral-cluster"} export KUBECONFIG_TARGET_CONTEXT=${KUBECONFIG_TARGET_CONTEXT:-"target-cluster"} -export TARGET_NODE=${TARGET_NODE:-"node01"} export CLUSTER_NAMESPACE=${CLUSTER_NAMESPACE:-"default"} -echo "Check Cluster Status" -kubectl --kubeconfig $KUBECONFIG --context $KUBECONFIG_EPHEMERAL_CONTEXT -n $CLUSTER_NAMESPACE get cluster target-cluster -o json | jq '.status.controlPlaneReady' - -echo "Annotate BMH for target node" -kubectl --kubeconfig $KUBECONFIG --context $KUBECONFIG_EPHEMERAL_CONTEXT -n $CLUSTER_NAMESPACE annotate bmh $TARGET_NODE baremetalhost.metal3.io/paused=true +# Annotating BMH objects with a pause label +# Scripts for this phase placed in manifests/function/phase-helpers/pause_bmh/ +# To get ConfigMap for this phase, execute `airshipctl phase render --source config -k ConfigMap` +# and find ConfigMap with name kubectl-get-pods +airshipctl phase run kubectl-pause-bmh --debug echo "Move Cluster Object to Target Cluster" airshipctl phase run clusterctl-move