From 6523c800ad2d9c593cca67a86e8b3cc7eda3f6f9 Mon Sep 17 00:00:00 2001 From: Vladimir Kozhukalov Date: Sat, 6 Feb 2021 12:15:00 +0300 Subject: [PATCH] Build ephemeral iso using generic container We build iso image in two steps 1) We prepare cloud-init data using a krm function krm-functions/cloud-init which uses arishipctl capabilities to gather necessary data from the executor document bundle. Cloud-init data files are written into a directory mounted to the krm function container. 2) We build iso image using image-builder. While doing this we mount the directory with cloud-init data files and set necessary environment variables defined in the executor document. Relates-To: #440 Change-Id: Id0b34822e95f494d2e2f8fb407700b7f873e7c69 --- krm-functions/cloud-init/Dockerfile.sample | 11 ++ krm-functions/cloud-init/Makefile | 78 +++++++++++ krm-functions/cloud-init/README.md | 6 + krm-functions/cloud-init/image/go.mod | 9 ++ .../example-input-resource-list.yaml | 32 +++++ krm-functions/cloud-init/main.go | 128 ++++++++++++++++++ manifests/phases/executors.yaml | 72 ++++++++++ manifests/phases/phases.yaml | 22 +++ manifests/phases/plan.yaml | 11 ++ .../site/test-site/empty/kustomization.yaml | 0 pkg/api/v1alpha1/genericcontainer_types.go | 2 +- 11 files changed, 370 insertions(+), 1 deletion(-) create mode 100644 krm-functions/cloud-init/Dockerfile.sample create mode 100644 krm-functions/cloud-init/Makefile create mode 100644 krm-functions/cloud-init/README.md create mode 100644 krm-functions/cloud-init/image/go.mod create mode 100644 krm-functions/cloud-init/local-resource/example-input-resource-list.yaml create mode 100644 krm-functions/cloud-init/main.go create mode 100644 manifests/site/test-site/empty/kustomization.yaml diff --git a/krm-functions/cloud-init/Dockerfile.sample b/krm-functions/cloud-init/Dockerfile.sample new file mode 100644 index 000000000..d114a6758 --- /dev/null +++ b/krm-functions/cloud-init/Dockerfile.sample @@ -0,0 +1,11 @@ +FROM gcr.io/gcp-runtimes/go1-builder:1.13 as builder +ENV CGO_ENABLED=0 +WORKDIR /go/src/ +COPY image/go.mod . +RUN /usr/local/go/bin/go mod download +COPY main.go . +RUN /usr/local/go/bin/go build -v -o /usr/local/bin/config-function ./ + +FROM alpine:latest +COPY --from=builder /usr/local/bin/config-function /usr/local/bin/config-function +CMD ["/usr/local/bin/config-function"] diff --git a/krm-functions/cloud-init/Makefile b/krm-functions/cloud-init/Makefile new file mode 100644 index 000000000..7427cad6d --- /dev/null +++ b/krm-functions/cloud-init/Makefile @@ -0,0 +1,78 @@ +.PHONY: generate license fix vet fmt test build tidy image + +SHELL := /bin/bash +GOBIN := $(shell go env GOPATH)/bin + +# docker image options +DOCKER_REGISTRY ?= quay.io +DOCKER_IMAGE_NAME ?= cloud-init +DOCKER_IMAGE_PREFIX ?= airshipit +DOCKER_IMAGE_TAG ?= latest +DOCKER_IMAGE ?= $(DOCKER_REGISTRY)/$(DOCKER_IMAGE_PREFIX)/$(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG) +PUBLISH ?= false +DOCKER_FORCE_CLEAN ?= true + +# proxy options +PROXY ?= http://proxy.foo.com:8000 +NO_PROXY ?= localhost,127.0.0.1,.svc.cluster.local +USE_PROXY ?= false + +.PHONY: build +build: + (cd image && go build -v -o $(GOBIN)/config-function .) + +.PHONY: all +all: generate license build fix vet fmt test lint tidy + +.PHONY: fix +fix: + (cd image && go fix ./...) + +.PHONY: fmt +fmt: + (cd image && go fmt ./...) + +.PHONY: generate +generate: + (which $(GOBIN)/mdtogo || go get sigs.k8s.io/kustomize/cmd/mdtogo) + (cd image && GOBIN=$(GOBIN) go generate ./...) + +.PHONY: tidy +tidy: + (cd image && go mod tidy) + +.PHONY: fix +lint: + (which $(GOBIN)/golangci-lint || go get github.com/golangci/golangci-lint/cmd/golangci-lint@v1.19.1) + (cd image && $(GOBIN)/golangci-lint run ./...) + +.PHONY: test +test: + (cd image && go test -cover ./...) + +.PHONY: vet +vet: + (cd image && go vet ./...) + +.PHONY: image +image: +ifeq ($(USE_PROXY), true) + cd image && \ + docker build . --network=host \ + --build-arg http_proxy=$(PROXY) \ + --build-arg https_proxy=$(PROXY) \ + --build-arg HTTP_PROXY=$(PROXY) \ + --build-arg HTTPS_PROXY=$(PROXY) \ + --build-arg no_proxy=$(NO_PROXY) \ + --build-arg NO_PROXY=$(NO_PROXY) \ + --tag $(DOCKER_IMAGE) \ + --force-rm=$(DOCKER_FORCE_CLEAN) +else + cd image && \ + docker build . --network=host \ + --tag $(DOCKER_IMAGE) \ + --force-rm=$(DOCKER_FORCE_CLEAN) +endif +ifeq ($(PUBLISH), true) + @docker push $(DOCKER_IMAGE) +endif diff --git a/krm-functions/cloud-init/README.md b/krm-functions/cloud-init/README.md new file mode 100644 index 000000000..02175e3eb --- /dev/null +++ b/krm-functions/cloud-init/README.md @@ -0,0 +1,6 @@ +# Cloud-init + +This function generates yaml files (user-data and network-data) needed for building the bootstrap ISO image. +It assumes a `ResourceList` is passed to its input where `items` field is a phase executor document bundle +that contains necessary data. To get the data from the bundle the `functionConfig` field must contain +`IsoConfiguration` document that defines document selector parameters. diff --git a/krm-functions/cloud-init/image/go.mod b/krm-functions/cloud-init/image/go.mod new file mode 100644 index 000000000..a30c7a942 --- /dev/null +++ b/krm-functions/cloud-init/image/go.mod @@ -0,0 +1,9 @@ +module opendev.org/airship/airshipctl/functions/cloud-init/image + +go 1.14 + +require ( + opendev.org/airship/airshipctl v0.0.0-20210217205206-b8a4b6ad734c + sigs.k8s.io/kustomize/kyaml v0.10.0 + sigs.k8s.io/kustomize/api v0.7.2 +) diff --git a/krm-functions/cloud-init/local-resource/example-input-resource-list.yaml b/krm-functions/cloud-init/local-resource/example-input-resource-list.yaml new file mode 100644 index 000000000..a695a7eb2 --- /dev/null +++ b/krm-functions/cloud-init/local-resource/example-input-resource-list.yaml @@ -0,0 +1,32 @@ +# 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. +apiVersion: config.kubernetes.io/v1alpha1 +kind: ResourceList +items: [] +functionConfig: + apiVersion: airshipit.org/v1alpha1 + kind: IsoConfiguration + metadata: + annotations: + config.kubernetes.io/function: |- + container: + image: krm-function-cloud-init:latest + name: cloud-init + builder: + userDataSelector: + kind: Secret + labelSelector: airshipit.org/ephemeral-user-data + userDataKey: userData + networkConfigSelector: + kind: BareMetalHost + labelSelector: airshipit.org/ephemeral-node + networkConfigKey: networkData diff --git a/krm-functions/cloud-init/main.go b/krm-functions/cloud-init/main.go new file mode 100644 index 000000000..22615a84e --- /dev/null +++ b/krm-functions/cloud-init/main.go @@ -0,0 +1,128 @@ +/* + 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 ( + "errors" + "fmt" + "os" + "path/filepath" + + "sigs.k8s.io/kustomize/api/provider" + "sigs.k8s.io/kustomize/api/resmap" + "sigs.k8s.io/kustomize/kyaml/fn/framework" + "sigs.k8s.io/kustomize/kyaml/fn/runtime/runtimeutil" + "sigs.k8s.io/kustomize/kyaml/yaml" + + "opendev.org/airship/airshipctl/pkg/api/v1alpha1" + "opendev.org/airship/airshipctl/pkg/bootstrap/cloudinit" + "opendev.org/airship/airshipctl/pkg/document" + "opendev.org/airship/airshipctl/pkg/util" +) + +const ( + builderConfigFileName = "builder-conf.yaml" + userDataFileName = "user-data" + networkConfigFileName = "network-data" +) + +func bundleFromRNodes(rnodes []*yaml.RNode) (document.Bundle, error) { + p := provider.NewDefaultDepProvider() + resmapFactory := resmap.NewFactory(p.GetResourceFactory(), p.GetConflictDetectorFactory()) + resmap, err := resmapFactory.NewResMapFromRNodeSlice(rnodes) + if err != nil { + return &document.BundleFactory{}, err + } + return &document.BundleFactory{ + ResMap: resmap, + }, nil +} + +func docFromRNode(rnode *yaml.RNode) (document.Document, error) { + rnodes := []*yaml.RNode{rnode} + bundle, err := bundleFromRNodes(rnodes) + if err != nil { + return nil, err + } + collection, err := bundle.GetAllDocuments() + if err != nil { + return nil, err + } + if len(collection) == 0 { + return nil, errors.New("Error while converting RNode to Document: empty document bundle") + } + return collection[0], nil +} + +func main() { + resourceList := &framework.ResourceList{} + cmd := framework.Command(resourceList, func() error { + functionConfig, ok := resourceList.FunctionConfig.(*yaml.RNode) + if !ok { + return errors.New("Error while type assert of FunctionConfig") + } + + functionConfigDocument, err := docFromRNode(functionConfig) + if err != nil { + return err + } + functionConfigYaml, err := functionConfigDocument.AsYAML() + if err != nil { + return err + } + + isoConfiguration := &v1alpha1.IsoConfiguration{} + err = functionConfigDocument.ToAPIObject(isoConfiguration, v1alpha1.Scheme) + if err != nil { + return err + } + + docBundle, err := bundleFromRNodes(resourceList.Items) + if err != nil { + return err + } + + userData, netConf, err := cloudinit.GetCloudData( + docBundle, + isoConfiguration.Isogen.UserDataSelector, + isoConfiguration.Isogen.UserDataKey, + isoConfiguration.Isogen.NetworkConfigSelector, + isoConfiguration.Isogen.NetworkConfigKey, + ) + if err != nil { + return err + } + + functionSpec := runtimeutil.GetFunctionSpec(functionConfig) + configPath := functionSpec.Container.StorageMounts[0].DstPath + + fls := make(map[string][]byte) + fls[filepath.Join(configPath, userDataFileName)] = userData + fls[filepath.Join(configPath, networkConfigFileName)] = netConf + fls[filepath.Join(configPath, builderConfigFileName)] = functionConfigYaml + + if err = util.WriteFiles(fls, 0600); err != nil { + return err + } + + resourceList.Items = []*yaml.RNode{} + return nil + }) + + if err := cmd.Execute(); err != nil { + fmt.Fprint(os.Stderr, err) + os.Exit(1) + } +} diff --git a/manifests/phases/executors.yaml b/manifests/phases/executors.yaml index e8429f65f..1957d89a6 100644 --- a/manifests/phases/executors.yaml +++ b/manifests/phases/executors.yaml @@ -184,3 +184,75 @@ spec: remoteDirect: isoURL: REPLACE_ME --- +apiVersion: airshipit.org/v1alpha1 +kind: GenericContainer +metadata: + name: iso-cloud-init-data + labels: + airshipit.org/deploy-k8s: "false" +spec: + type: krm + image: quay.io/airshipit/cloud-init:latest + mounts: + - type: bind + src: /srv/iso + dst: /config + rw: true +config: | + apiVersion: airshipit.org/v1alpha1 + kind: IsoConfiguration + metadata: + name: isogen + builder: + userDataSelector: + kind: Secret + labelSelector: airshipit.org/ephemeral-user-data + userDataKey: userData + networkConfigSelector: + kind: BareMetalHost + labelSelector: airshipit.org/ephemeral-node + networkConfigKey: networkData + outputFileName: ephemeral.iso + container: + volume: /fake/path/iso:/config # for compatibility with image-builder + +--- +apiVersion: airshipit.org/v1alpha1 +kind: GenericContainer +metadata: + name: iso-build-image + labels: + airshipit.org/deploy-k8s: "false" +spec: + type: airship + airship: + privileged: true + containerRuntime: docker + cmd: + - /bin/bash + - -c + - /usr/bin/local/entrypoint.sh 1>&2 + image: quay.io/airshipit/image-builder:latest-ubuntu_focal + mounts: + - type: bind + src: /srv/iso + dst: /config + rw: true + envVars: + - IMAGE_TYPE=iso + - BUILDER_CONFIG=/config/builder-conf.yaml + - USER_DATA_FILE=user-data + - NET_CONFIG_FILE=network-data + - OUTPUT_FILE_NAME=ephemerial.iso + - OUTPUT_METADATA_FILE_NAME=output-metadata.yaml + - http_proxy + - https_proxy + - HTTP_PROXY + - HTTPS_PROXY + - no_proxy + - NO_PROXY +config: | + apiVersion: airshipit.org/v1alpha1 + kind: DoesNotMatter + metadata: + name: isogen diff --git a/manifests/phases/phases.yaml b/manifests/phases/phases.yaml index c9d929358..4fc17a347 100644 --- a/manifests/phases/phases.yaml +++ b/manifests/phases/phases.yaml @@ -246,3 +246,25 @@ config: apiVersion: airshipit.org/v1alpha1 kind: BaremetalManager name: RemoteDirectEphemeral +--- +apiVersion: airshipit.org/v1alpha1 +kind: Phase +metadata: + name: iso-cloud-init-data +config: + executorRef: + apiVersion: airshipit.org/v1alpha1 + kind: GenericContainer + name: iso-cloud-init-data + documentEntryPoint: ephemeral/bootstrap +--- +apiVersion: airshipit.org/v1alpha1 +kind: Phase +metadata: + name: iso-build-image +config: + executorRef: + apiVersion: airshipit.org/v1alpha1 + kind: GenericContainer + name: iso-build-image + documentEntryPoint: empty diff --git a/manifests/phases/plan.yaml b/manifests/phases/plan.yaml index b64aefac1..778aefe92 100644 --- a/manifests/phases/plan.yaml +++ b/manifests/phases/plan.yaml @@ -14,3 +14,14 @@ phases: - name: workers-target - name: workers-classification - name: workload-target + +--- +apiVersion: airshipit.org/v1alpha1 +kind: PhasePlan +metadata: + name: iso +description: "Runs phases to build iso image" +phases: + - name: iso-cloud-init-data + - name: iso-build-image + diff --git a/manifests/site/test-site/empty/kustomization.yaml b/manifests/site/test-site/empty/kustomization.yaml new file mode 100644 index 000000000..e69de29bb diff --git a/pkg/api/v1alpha1/genericcontainer_types.go b/pkg/api/v1alpha1/genericcontainer_types.go index 4008a6cc6..2db5800f5 100644 --- a/pkg/api/v1alpha1/genericcontainer_types.go +++ b/pkg/api/v1alpha1/genericcontainer_types.go @@ -96,7 +96,7 @@ type AirshipContainerSpec struct { Cmd []string `json:"cmd,omitempty"` // Privileged identifies if the container is to be run in a Privileged mode - Privileged bool `json:"pivileged,omitempty"` + Privileged bool `json:"privileged,omitempty"` } // KRMContainerSpec defines a spec for running a function as a container