Merge "Add toolbox krm function"
This commit is contained in:
commit
ea534c379f
2
Makefile
2
Makefile
@ -155,6 +155,8 @@ ifeq ($(PUBLISH), true)
|
|||||||
@docker push $(DOCKER_IMAGE)
|
@docker push $(DOCKER_IMAGE)
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
# Use specific Dockerfile instead of general one to make image for toolbox
|
||||||
|
docker-image-toolbox: DOCKER_CMD_FLAGS+=-f krm-functions/toolbox/Dockerfile
|
||||||
.PHONY: $(PLUGINS_IMAGE_TGT)
|
.PHONY: $(PLUGINS_IMAGE_TGT)
|
||||||
$(PLUGINS_IMAGE_TGT):
|
$(PLUGINS_IMAGE_TGT):
|
||||||
$(eval plugin_name=$(subst docker-image-,,$@))
|
$(eval plugin_name=$(subst docker-image-,,$@))
|
||||||
|
26
krm-functions/toolbox/Dockerfile
Normal file
26
krm-functions/toolbox/Dockerfile
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
ARG RELEASE_IMAGE=scratch
|
||||||
|
FROM ${RELEASE_IMAGE} as kctl
|
||||||
|
RUN apk add curl
|
||||||
|
RUN curl -L "https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl" \
|
||||||
|
-o /kubectl
|
||||||
|
RUN chmod +x /kubectl
|
||||||
|
|
||||||
|
FROM gcr.io/gcp-runtimes/go1-builder:1.13 as builder
|
||||||
|
ENV CGO_ENABLED=0
|
||||||
|
WORKDIR /go/src/
|
||||||
|
COPY krm-functions/toolbox/image/go.mod .
|
||||||
|
RUN /usr/local/go/bin/go mod download
|
||||||
|
COPY krm-functions/toolbox/main.go .
|
||||||
|
RUN /usr/local/go/bin/go build -v -o /usr/local/bin/config-function ./
|
||||||
|
|
||||||
|
FROM ${RELEASE_IMAGE} as calicoctl
|
||||||
|
RUN apk add curl
|
||||||
|
RUN curl -L "https://github.com/projectcalico/calicoctl/releases/download/v3.18.1/calicoctl" \
|
||||||
|
-o /calicoctl
|
||||||
|
RUN chmod +x /calicoctl
|
||||||
|
|
||||||
|
FROM ${RELEASE_IMAGE} as release
|
||||||
|
COPY --from=kctl /kubectl /usr/local/bin/kubectl
|
||||||
|
COPY --from=calicoctl /calicoctl /usr/local/bin/calicoctl
|
||||||
|
COPY --from=builder /usr/local/bin/config-function /usr/local/bin/config-function
|
||||||
|
CMD ["/usr/local/bin/config-function"]
|
8
krm-functions/toolbox/image/go.mod
Normal file
8
krm-functions/toolbox/image/go.mod
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
module opendev.org/airship/airshipctl/krm-fnunctions/toolbox/image
|
||||||
|
|
||||||
|
go 1.14
|
||||||
|
|
||||||
|
require (
|
||||||
|
k8s.io/api v0.17.9
|
||||||
|
sigs.k8s.io/kustomize/kyaml v0.10.6
|
||||||
|
)
|
151
krm-functions/toolbox/main.go
Normal file
151
krm-functions/toolbox/main.go
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
/*
|
||||||
|
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
|
||||||
|
|
||||||
|
https://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 main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
v1 "k8s.io/api/core/v1"
|
||||||
|
kerror "k8s.io/apimachinery/pkg/util/errors"
|
||||||
|
"sigs.k8s.io/kustomize/kyaml/fn/framework"
|
||||||
|
"sigs.k8s.io/kustomize/kyaml/kio"
|
||||||
|
kyaml "sigs.k8s.io/kustomize/kyaml/yaml"
|
||||||
|
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cfg := &v1.ConfigMap{}
|
||||||
|
resourceList := &framework.ResourceList{FunctionConfig: &cfg}
|
||||||
|
runner := ScriptRunner{
|
||||||
|
ScriptFile: scriptPath,
|
||||||
|
WorkDir: workdir,
|
||||||
|
RenderedBundleFile: bundleFile,
|
||||||
|
DataKey: scriptKey,
|
||||||
|
ResourceList: resourceList,
|
||||||
|
ConfigMap: cfg,
|
||||||
|
ErrStream: os.Stderr,
|
||||||
|
OutStream: os.Stdout,
|
||||||
|
}
|
||||||
|
cmd := framework.Command(resourceList, runner.Run)
|
||||||
|
if err := cmd.Execute(); err != nil {
|
||||||
|
log.Print(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScriptRunner writes to file system and executes the script
|
||||||
|
type ScriptRunner struct {
|
||||||
|
ScriptFile, WorkDir, DataKey, RenderedBundleFile string
|
||||||
|
|
||||||
|
ErrStream io.Writer
|
||||||
|
OutStream io.Writer
|
||||||
|
|
||||||
|
ConfigMap *v1.ConfigMap
|
||||||
|
ResourceList *framework.ResourceList
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run writes the script and bundle to the file system and executes it
|
||||||
|
func (c *ScriptRunner) Run() error {
|
||||||
|
bundlePath, scriptPath := c.getBundleAndScriptPath()
|
||||||
|
|
||||||
|
script, exist := c.ConfigMap.Data[c.DataKey]
|
||||||
|
if !exist {
|
||||||
|
return fmt.Errorf("ConfigMap '%s/%s' doesnt' have specified script key '%s'",
|
||||||
|
c.ConfigMap.Namespace, c.ConfigMap.Name, c.DataKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := ioutil.WriteFile(scriptPath, []byte(script), 0555)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = c.writeBundle(bundlePath, c.ResourceList.Items)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.ResourceList.Items = nil
|
||||||
|
|
||||||
|
os.Setenv(EnvRenderedBundlePath, bundlePath)
|
||||||
|
|
||||||
|
clicmd := exec.Command(scriptPath)
|
||||||
|
clicmd.Stdout = c.OutStream
|
||||||
|
clicmd.Stderr = c.ErrStream
|
||||||
|
|
||||||
|
err = clicmd.Start()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return clicmd.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup removes script and bundle files from filesystem
|
||||||
|
func (c *ScriptRunner) Cleanup() error {
|
||||||
|
bundlePath, scriptPath := c.getBundleAndScriptPath()
|
||||||
|
|
||||||
|
scriptErr := os.Remove(scriptPath)
|
||||||
|
if os.IsNotExist(scriptErr) {
|
||||||
|
// If file doesn't exist no error happened
|
||||||
|
scriptErr = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
bundleErr := os.Remove(bundlePath)
|
||||||
|
if os.IsNotExist(bundleErr) {
|
||||||
|
// If file doesn't exist no error happened
|
||||||
|
bundleErr = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return kerror.NewAggregate([]error{scriptErr, bundleErr})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ScriptRunner) getBundleAndScriptPath() (string, string) {
|
||||||
|
return filepath.Join(c.WorkDir, c.RenderedBundleFile), filepath.Join(c.WorkDir, c.ScriptFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ScriptRunner) writeBundle(path string, items []*kyaml.RNode) error {
|
||||||
|
f, err := os.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
pipeline := kio.Pipeline{
|
||||||
|
Outputs: []kio.Writer{
|
||||||
|
kio.ByteWriter{
|
||||||
|
Writer: f,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Inputs: []kio.Reader{
|
||||||
|
kio.ResourceNodeSlice(items),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return pipeline.Execute()
|
||||||
|
}
|
168
krm-functions/toolbox/main_test.go
Normal file
168
krm-functions/toolbox/main_test.go
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
/*
|
||||||
|
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
|
||||||
|
|
||||||
|
https://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 main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io/ioutil"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
v1 "k8s.io/api/core/v1"
|
||||||
|
"sigs.k8s.io/kustomize/kyaml/fn/framework"
|
||||||
|
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
dir = "image"
|
||||||
|
targetFile = "my-script.sh"
|
||||||
|
dataKey = "script"
|
||||||
|
wrongDataKey = "foobar"
|
||||||
|
bundlePath = "bundle.yaml"
|
||||||
|
script = `#!/bin/bash
|
||||||
|
echo -n 'stderr' 1>&2
|
||||||
|
echo -n 'stdout'`
|
||||||
|
wrongScript = `#!/usr/bin/p
|
||||||
|
print("Hello world!")`
|
||||||
|
inputString = `kind: testkind
|
||||||
|
metadata:
|
||||||
|
name: test-name
|
||||||
|
namespace: test-namespace
|
||||||
|
`
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCmdRun(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
workdir string
|
||||||
|
errContains string
|
||||||
|
configMap *v1.ConfigMap
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Successful run",
|
||||||
|
workdir: dir,
|
||||||
|
configMap: &v1.ConfigMap{
|
||||||
|
Data: map[string]string{
|
||||||
|
dataKey: script,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Wrong key in ConfigMap",
|
||||||
|
workdir: dir,
|
||||||
|
errContains: "ConfigMap '/' doesnt' have specified script key 'script'",
|
||||||
|
configMap: &v1.ConfigMap{
|
||||||
|
Data: map[string]string{
|
||||||
|
wrongDataKey: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "WorkDir that doesnt' exist",
|
||||||
|
workdir: "foobar",
|
||||||
|
errContains: "open foobar/my-script.sh: no such file or directory",
|
||||||
|
configMap: &v1.ConfigMap{
|
||||||
|
Data: map[string]string{
|
||||||
|
dataKey: script,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Wrong interpreter",
|
||||||
|
workdir: dir,
|
||||||
|
errContains: "fork/exec image/my-script.sh: no such file or directory",
|
||||||
|
configMap: &v1.ConfigMap{
|
||||||
|
Data: map[string]string{
|
||||||
|
dataKey: wrongScript,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
tt := tt
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
input, err := yaml.Parse(inputString)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
stderr := bytes.NewBuffer([]byte{})
|
||||||
|
stdout := bytes.NewBuffer([]byte{})
|
||||||
|
|
||||||
|
cmd := &ScriptRunner{
|
||||||
|
ScriptFile: targetFile,
|
||||||
|
WorkDir: tt.workdir,
|
||||||
|
DataKey: dataKey,
|
||||||
|
ErrStream: stderr,
|
||||||
|
OutStream: stdout,
|
||||||
|
ResourceList: &framework.ResourceList{Items: []*yaml.RNode{input}},
|
||||||
|
ConfigMap: tt.configMap,
|
||||||
|
RenderedBundleFile: bundlePath,
|
||||||
|
}
|
||||||
|
err = cmd.Run()
|
||||||
|
defer func() {
|
||||||
|
require.NoError(t, cmd.Cleanup())
|
||||||
|
}()
|
||||||
|
|
||||||
|
if tt.errContains != "" {
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), tt.errContains)
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "stderr", stderr.String())
|
||||||
|
assert.Equal(t, "stdout", stdout.String())
|
||||||
|
bundleFullPath := filepath.Join(tt.workdir, bundlePath)
|
||||||
|
assert.FileExists(t, bundleFullPath)
|
||||||
|
result, err := ioutil.ReadFile(filepath.Join(tt.workdir, bundlePath))
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Contains(t, string(result), "testkind")
|
||||||
|
assert.Contains(t, string(result), "test-name")
|
||||||
|
assert.Contains(t, string(result), "test-namespace")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCmdRunCleanup(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{})
|
||||||
|
|
||||||
|
cmd := &ScriptRunner{
|
||||||
|
ScriptFile: targetFile,
|
||||||
|
WorkDir: dir,
|
||||||
|
DataKey: dataKey,
|
||||||
|
ErrStream: stderr,
|
||||||
|
OutStream: stdout,
|
||||||
|
ResourceList: &framework.ResourceList{Items: []*yaml.RNode{input}},
|
||||||
|
ConfigMap: cMap,
|
||||||
|
RenderedBundleFile: bundlePath,
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(t, cmd.Cleanup())
|
||||||
|
err = cmd.Run()
|
||||||
|
defer func() {
|
||||||
|
require.NoError(t, cmd.Cleanup())
|
||||||
|
}()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
@ -273,3 +273,18 @@ config: |
|
|||||||
kind: DoesNotMatter
|
kind: DoesNotMatter
|
||||||
metadata:
|
metadata:
|
||||||
name: isogen
|
name: isogen
|
||||||
|
---
|
||||||
|
apiVersion: airshipit.org/v1alpha1
|
||||||
|
kind: GenericContainer
|
||||||
|
metadata:
|
||||||
|
name: kubectl-get-node
|
||||||
|
labels:
|
||||||
|
airshipit.org/deploy-k8s: "false"
|
||||||
|
spec:
|
||||||
|
type: krm
|
||||||
|
image: quay.io/airshipit/toolbox:latest
|
||||||
|
hostNetwork: true
|
||||||
|
configRef:
|
||||||
|
kind: ConfigMap
|
||||||
|
name: kubectl-get-node
|
||||||
|
apiVersion: v1
|
||||||
|
@ -278,3 +278,15 @@ config:
|
|||||||
apiVersion: airshipit.org/v1alpha1
|
apiVersion: airshipit.org/v1alpha1
|
||||||
kind: GenericContainer
|
kind: GenericContainer
|
||||||
name: iso-build-image
|
name: iso-build-image
|
||||||
|
---
|
||||||
|
apiVersion: airshipit.org/v1alpha1
|
||||||
|
kind: Phase
|
||||||
|
metadata:
|
||||||
|
name: kubectl-get-node-ephemeral
|
||||||
|
clusterName: ephemeral-cluster
|
||||||
|
config:
|
||||||
|
executorRef:
|
||||||
|
apiVersion: airshipit.org/v1alpha1
|
||||||
|
kind: GenericContainer
|
||||||
|
name: kubectl-get-node
|
||||||
|
documentEntryPoint: ephemeral/initinfra-networking
|
||||||
|
32
manifests/site/test-site/phases/helpers/kubectl_get_node.sh
Normal file
32
manifests/site/test-site/phases/helpers/kubectl_get_node.sh
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
#!/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.
|
||||||
|
|
||||||
|
N=0
|
||||||
|
MAX_RETRY=30
|
||||||
|
DELAY=60
|
||||||
|
until [ "$N" -ge ${MAX_RETRY} ]
|
||||||
|
do
|
||||||
|
if timeout 20 kubectl --kubeconfig $KUBECONFIG --context $KCTL_CONTEXT get node 1>&2; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
|
||||||
|
N=$((N+1))
|
||||||
|
echo "$N: Retrying to reach the apiserver" 1>&2
|
||||||
|
sleep ${DELAY}
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$N" -ge ${MAX_RETRY} ]; then
|
||||||
|
echo "Could not reach the apiserver" 1>&2
|
||||||
|
exit 1
|
||||||
|
fi
|
@ -0,0 +1,6 @@
|
|||||||
|
configMapGenerator:
|
||||||
|
- name: kubectl-get-node
|
||||||
|
options:
|
||||||
|
disableNameSuffixHash: true
|
||||||
|
files:
|
||||||
|
- script=kubectl_get_node.sh
|
@ -2,5 +2,6 @@ resources:
|
|||||||
- ../kubeconfig
|
- ../kubeconfig
|
||||||
- ../../../phases
|
- ../../../phases
|
||||||
- catalogue.yaml
|
- catalogue.yaml
|
||||||
|
- helpers
|
||||||
transformers:
|
transformers:
|
||||||
- ../../../function/bootstrap/replacements
|
- ../../../function/bootstrap/replacements
|
||||||
|
@ -23,24 +23,7 @@ echo "Deploy ephemeral node using redfish with iso"
|
|||||||
airshipctl phase run remotedirect-ephemeral --debug
|
airshipctl phase run remotedirect-ephemeral --debug
|
||||||
|
|
||||||
echo "Wait for apiserver to become available"
|
echo "Wait for apiserver to become available"
|
||||||
N=0
|
airshipctl phase run kubectl-get-node-ephemeral
|
||||||
MAX_RETRY=30
|
|
||||||
DELAY=60
|
|
||||||
until [ "$N" -ge ${MAX_RETRY} ]
|
|
||||||
do
|
|
||||||
if timeout 20 kubectl --kubeconfig $KUBECONFIG --context $KUBECONFIG_EPHEMERAL_CONTEXT get node; then
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
|
|
||||||
N=$((N+1))
|
|
||||||
echo "$N: Retrying to reach the apiserver"
|
|
||||||
sleep ${DELAY}
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ "$N" -ge ${MAX_RETRY} ]; then
|
|
||||||
echo "Could not reach the apiserver"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "List all pods"
|
echo "List all pods"
|
||||||
kubectl --kubeconfig $KUBECONFIG --context $KUBECONFIG_EPHEMERAL_CONTEXT get pods --all-namespaces
|
kubectl --kubeconfig $KUBECONFIG --context $KUBECONFIG_EPHEMERAL_CONTEXT get pods --all-namespaces
|
||||||
|
Loading…
x
Reference in New Issue
Block a user