diff --git a/.gitignore b/.gitignore index 12c2a1f..936d613 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ *build/ bootstrap_capg/.vscode/launch.json bootstrap_capg/go.mod +bootstrap_capz/.vscode/launch.json +bootstrap_capz/go.mod +bootstrap_capz/go.sum diff --git a/bootstrap_capz/Dockerfile b/bootstrap_capz/Dockerfile new file mode 100644 index 0000000..e5ca084 --- /dev/null +++ b/bootstrap_capz/Dockerfile @@ -0,0 +1,57 @@ +# Copyright 2018 AT&T Intellectual Property. All other rights reserved. +# +# 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. + +ARG AZ_SDK=mcr.microsoft.com/azure-cli:2.8.0 +ARG GOLANG=golang:1.14.4 + +############################################ +# Build GCP Bootstrap Container application +############################################ +FROM ${GOLANG} as builder +WORKDIR /home/build +# copy the capg bootstrap container app code +COPY main.go . +COPY config/ config/ +# Build capg bootstrap container application +RUN go mod init opendev.org/airship/images/bootstrap_capz && \ + go get -d -v ./... && \ + go install . && \ + CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o capz-ephemeral . + +############################################ +# Run Azure Bootstrap Container +############################################ +FROM ${AZ_SDK} + +LABEL org.opencontainers.image.authors='airship-discuss@lists.airshipit.org, irc://#airshipit@freenode' \ + org.opencontainers.image.url='https://airshipit.org' \ + org.opencontainers.image.documentation='https://opendev.org/airship/images/src/branch/master/bootstrap_capg/README.md' \ + org.opencontainers.image.source='https://opendev.org/airship/images' \ + org.opencontainers.image.vendor='The Airship Authors' \ + org.opencontainers.image.licenses='Apache-2.0' + +RUN adduser --disabled-password --gecos "" bootstrap +USER bootstrap + +WORKDIR /home/bootstrap +ENV HOME=/home/bootstrap +ENV PATH="${PATH}:${HOME}" + +# Copy the Azure Bootstrap Container command +COPY --from=builder /home/build/capz-ephemeral . +# Copy help file +COPY assets/help.txt . + +# Executes the Azure Bootstrap command +CMD ["capz-ephemeral"] diff --git a/bootstrap_capz/Makefile b/bootstrap_capz/Makefile new file mode 100644 index 0000000..5e5548e --- /dev/null +++ b/bootstrap_capz/Makefile @@ -0,0 +1,56 @@ +SHELL := /bin/bash +PUSH_IMAGE ?= false +GIT_VERSION ?= v0.1.0 +GIT_MODULE ?= opendev.org/airship/airshipctl/pkg/version + +GO_FLAGS := -ldflags '-extldflags "-static"' -tags=netgo +GO_FLAGS += -ldflags "-X ${GIT_MODULE}.gitVersion=${GIT_VERSION}" + +DOCKER_MAKE_TARGET := build + +# docker image options +DOCKER_REGISTRY ?= quay.io +DOCKER_FORCE_CLEAN ?= true +DOCKER_IMAGE_NAME ?= capz-bootstrap +DOCKER_IMAGE_PREFIX ?= airshipit +DOCKER_IMAGE_TAG ?= latest +DOCKER_IMAGE ?= $(DOCKER_REGISTRY)/$(DOCKER_IMAGE_PREFIX)/$(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG) +DOCKER_TARGET_STAGE ?= release + +PATH += :/usr/local/go/bin +HELP_FILE ?= /tmp/help.txt +ORIGINAL_HELP_FILE = ./assets/help.txt + +.PHONY: all +all: images + +.PHONY: images +images: Dockerfile \ + main.go \ + config/azure_cluster.go \ + config/aks_cluster.go \ + config/azure_config.go \ + assets/help.txt + @docker build . --network=host \ + --build-arg MAKE_TARGET=$(DOCKER_MAKE_TARGET) \ + --tag $(DOCKER_IMAGE) \ + --force-rm=$(DOCKER_FORCE_CLEAN) +ifeq ($(PUSH_IMAGE), true) + docker push $(DOCKER_IMAGE) +endif + +.PHONY: clean +clean: + @docker image rm $(DOCKER_IMAGE) + +.PHONY: lint +lint: + @echo TODO + +# style checks +.PHONY: tests +tests: images + if [ -f $(HELP_FILE) ]; then sudo rm $(HELP_FILE); fi + cp azure-config.yaml /tmp + docker run -v /tmp:/kube --env-file bootstrap-env.list --name capz-test $(DOCKER_IMAGE) + cmp $(HELP_FILE) $(ORIGINAL_HELP_FILE) diff --git a/bootstrap_capz/README.md b/bootstrap_capz/README.md new file mode 100644 index 0000000..7e740ef --- /dev/null +++ b/bootstrap_capz/README.md @@ -0,0 +1,32 @@ +# Azure Bootstrap Container + +This project contains the Go application and configuration files for +implementing the Azure Bootstrap container. + +The Azure Bootstrap container is responsible to create or delete a Kubernetes +(K8S) cluster on Azure Cloud platform using the AKS (Azure Kubernetes Service). + +## Go Application + +The Go application is the bootstrap container orchestrator that is responsible +for translating commands into actions: create, delete, help. + +This Go application uses the Ephemeral cluster configuration file +(e.g., azure-config.yaml) to determine the Azure credentials and +data to use to create or delete the ephemeral cluster. + +## Dockerfile + +The **Dockerfile** uses a multi-stage builds to first build the Go application +then create the Azure bootstrap container image. + +## Build + +To build the bootstrap container image, execute the following command: + +```bash +make images +``` + +This command will build the Go application and then create the bootstrap +container image. diff --git a/bootstrap_capz/assets/help.txt b/bootstrap_capz/assets/help.txt new file mode 100644 index 0000000..dac01ff --- /dev/null +++ b/bootstrap_capz/assets/help.txt @@ -0,0 +1,64 @@ + +Azure Ephemeral Configuration File Definition +--------------------------------------------- +The Azure Bootstrap container creates an Ephemeral K8S cluster on the Azure Cloud platform. +The container requires authentication credentials and other information about the cluster to deploy. +It requires a YAML configuration file with the format provided below. + + +apiVersion: v1 +kind: AzureConfig +metadata: + name: +credentials: + tenant: + client: + secret: +spec: + resourceGroup: + region: + cluster: + k8sVersion: + vmSize: + replicas: + kubeconfig: + + +It also accepts the JSON file format. + + +{ + "apiVersion":"v1", + "kind":"AzureConfig", + "metadata":{ + "name":"" + }, + "credentials":{ + "tenant":"", + "client":"", + "secret":"" + }, + "spec":{ + "resourceGroup":"", + "region":"", + "cluster":{ + "k8sVersion":"", + "vmSize":"", + "replicas":, + "kubeconfig":"" + } + } +} + + +The expected location for the Azure bootstrap configuration file is dictated by the "volume" mount +specified in the Airship config file (bootstrapInfo.ephemeral.container.volume). +For example, /home/esidshi/.airship folder and shown in the snippet below: + + +apiVersion: airshipit.org/v1alpha1 +bootstrapInfo: + ephemeral: + container: + volume: /home/esidshi/.airship:/kube + diff --git a/bootstrap_capz/azure-config.json b/bootstrap_capz/azure-config.json new file mode 100644 index 0000000..6afa44c --- /dev/null +++ b/bootstrap_capz/azure-config.json @@ -0,0 +1,22 @@ +{ + "apiVersion":"v1", + "credentials":{ + "tenant":"", + "client":"", + "secret":"" + }, + "kind":"AzureConfig", + "metadata":{ + "name":"capi-azure-zuul" + }, + "region":"centralus", + "resourceGroup":"airship2-zuul-rg", + "spec":{ + "cluster":{ + "k8sVersion":"1.18.6", + "kubeconfig":"capz.kubeconfig", + "replicas":1, + "vmSize":"Standard_B2s" + } + } +} \ No newline at end of file diff --git a/bootstrap_capz/azure-config.yaml b/bootstrap_capz/azure-config.yaml new file mode 100644 index 0000000..e4f7275 --- /dev/null +++ b/bootstrap_capz/azure-config.yaml @@ -0,0 +1,28 @@ +# 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: v1 +kind: AzureConfig +metadata: + name: capi-azure-zuul +credentials: + tenant: + client: + secret: +spec: + resourceGroup: airship2-zuul-rg + region: centralus + cluster: + k8sVersion: 1.18.6 + vmSize: Standard_B2s + replicas: 1 + kubeconfig: capz.kubeconfig diff --git a/bootstrap_capz/bootstrap-env.list b/bootstrap_capz/bootstrap-env.list new file mode 100644 index 0000000..dc57c41 --- /dev/null +++ b/bootstrap_capz/bootstrap-env.list @@ -0,0 +1,3 @@ +BOOTSTRAP_COMMAND=help +BOOTSTRAP_CONFIG=azure-config.yaml +BOOTSTRAP_VOLUME=/tmp:/kube \ No newline at end of file diff --git a/bootstrap_capz/config/aks_cluster.go b/bootstrap_capz/config/aks_cluster.go new file mode 100644 index 0000000..663316f --- /dev/null +++ b/bootstrap_capz/config/aks_cluster.go @@ -0,0 +1,165 @@ +/* + 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 config + +import ( + "log" + "os" + "strconv" +) + +const ( + az = "az" + aks = "aks" + login = "login" + group = "group" + create = "create" + delete = "delete" + getCredentials = "get-credentials" + servicePrincipalP = "--service-principal" + usernameP = "--username" + passwordP = "--password" + tenantP = "--tenant" + clientSecretP = "--client-secret" + resourceGroupP = "--resource-group" + locationP = "--location" + nameP = "--name" + nodeVMSizeP = "--node-vm-size" + nodeCountP = "--node-count" + k8sVersionP = "--kubernetes-version" + generateSSHKeysP = "--generate-ssh-keys" + fileP = "--file" + yesP = "--yes" + + defaultRegion = "centralus" + defaultResourceGroup = "airship2-aks-rg" + defaultClusterName = "capi-azure" + defaultVMSize = "Standard_B2s" + defaultK8SVersion = "1.18.6" + defaultKubeconfig = "kubeconfig" +) + +// defaulAzureConfig verify if any optional config data is missing. +func defaulAzureConfig(azConfig *AzureConfig) error { + if azConfig.Spec.Region == "" { + azConfig.Spec.Region = defaultRegion + } + if azConfig.Spec.ResourceGroup == "" { + azConfig.Spec.ResourceGroup = defaultResourceGroup + } + if azConfig.Metadata.Name == "" { + azConfig.Metadata.Name = defaultClusterName + } + if azConfig.Spec.Cluster.VMSize == "" { + azConfig.Spec.Cluster.VMSize = defaultVMSize + } + if azConfig.Spec.Cluster.K8SVersion == "" { + azConfig.Spec.Cluster.K8SVersion = defaultK8SVersion + } + if azConfig.Spec.Cluster.Kubeconfig == "" { + azConfig.Spec.Cluster.Kubeconfig = defaultKubeconfig + } + return nil +} + +// prepareAKSCluster logs in, create resource group, etc +func prepareAKSCluster(azConfig *AzureConfig, isCreate bool) error { + // Verify if azure config file provides all information needed for creating a cluster + err := defaulAzureConfig(azConfig) + if err != nil { + return err + } + + tenantID := azConfig.Credentials.Tenant + clientID := azConfig.Credentials.Client + clientSecret := azConfig.Credentials.Secret + region := azConfig.Spec.Region + resourceGroup := azConfig.Spec.ResourceGroup + clusterName := azConfig.Metadata.Name + vmSize := azConfig.Spec.Cluster.VMSize + nodeCount := strconv.FormatInt(int64(azConfig.Spec.Cluster.Replicas), 10) + k8sVersion := azConfig.Spec.Cluster.K8SVersion + kubeconfigFile := azConfig.Spec.Cluster.Kubeconfig + + // login to Azure account using Service Principal + err = execute(az, login, servicePrincipalP, + usernameP, clientID, + passwordP, clientSecret, + tenantP, tenantID) + if err != nil { + log.Printf("Failed to login into Azure using Service Principal\n") + return err + } + + if isCreate { + // Create resource group for the AKS cluster + err = execute(az, group, create, + resourceGroupP, resourceGroup, + locationP, region) + if err != nil { + log.Printf("Failed to create resource group %s in %s region.\n", resourceGroup, region) + return err + } + + // Creating Azure AKS cluster + err = execute(az, aks, create, + resourceGroupP, resourceGroup, + nameP, clusterName, + servicePrincipalP, clientID, + clientSecretP, clientSecret, + locationP, region, + nodeVMSizeP, vmSize, + nodeCountP, nodeCount, + k8sVersionP, k8sVersion, + generateSSHKeysP) + if err != nil { + log.Printf("Failed to create AKS cluster %s in %s region.\n", clusterName, region) + return err + } + + // Get Kubeconfig filename + volMount := os.Getenv("BOOTSTRAP_VOLUME") + _, dstMount := GetVolumeMountPoints(volMount) + + kubeconfig := dstMount + "/" + kubeconfigFile + + // Delete existing Kubeconfig file, if any + err = os.Remove(kubeconfig) + if err != nil { + log.Printf("Failed to remove existing kubeconfig file %s.\n", kubeconfig) + return err + } + + // Retrieving the Kubeconfig file for the cluster + err = execute(az, aks, getCredentials, + resourceGroupP, resourceGroup, + nameP, clusterName, + fileP, kubeconfig) + if err != nil { + log.Printf("Failed to retrieve kubeconfig file for AKS cluster %s in %s region.\n", clusterName, region) + return err + } + } else { + // Delete Azure AKS cluster + err = execute(az, aks, delete, + resourceGroupP, resourceGroup, + nameP, clusterName, yesP) + if err != nil { + log.Printf("Failed to delete AKS cluster %s in %s region.\n", clusterName, region) + return err + } + } + return nil +} diff --git a/bootstrap_capz/config/azure_cluster.go b/bootstrap_capz/config/azure_cluster.go new file mode 100644 index 0000000..dc26c62 --- /dev/null +++ b/bootstrap_capz/config/azure_cluster.go @@ -0,0 +1,101 @@ +/* + 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 config + +import ( + "io" + "log" + "os" + "os/exec" + "strings" +) + +const ( + bootstrapHelpFile = "help.txt" + + // BootstrapCommand environment variable + bootstrapHome = "HOME" + bootstrapCommand = "BOOTSTRAP_COMMAND" + bootstrapConfig = "BOOTSTRAP_CONFIG" + bootstrapVolume = "BOOTSTRAP_VOLUME" + bootstrapVolumeSep = ":" +) + +// GetVolumeMountPoints extracts the source and destination of a volume mount +func GetVolumeMountPoints(volumeMount string) (string, string) { + sepPos := strings.Index(volumeMount, bootstrapVolumeSep) + + srcMountPoint := volumeMount[:sepPos] + dstMountPoint := volumeMount[sepPos+1:] + + return srcMountPoint, dstMountPoint +} + +// Execute bash command +func execute(command string, arg ...string) error { + cmd := exec.Command(command, arg...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + if err := cmd.Start(); err != nil { + log.Printf("Error executing script %s\n", command) + return err + } + + if err := cmd.Wait(); err != nil { + log.Printf("Error waiting for command execution: %s", err.Error()) + return err + } + + return nil +} + +// CreateAKSCluster creates the AKS cluster +func CreateAKSCluster(config *AzureConfig) error { + return prepareAKSCluster(config, true) +} + +// DeleteAKSCluster deletes the AKS cluster +func DeleteAKSCluster(config *AzureConfig) error { + return prepareAKSCluster(config, false) +} + +// HelpAKSCluster returns the help.txt for the AKS cluster +func HelpAKSCluster() error { + homeDir := os.Getenv(bootstrapHome) + src := homeDir + "/" + bootstrapHelpFile + in, err := os.Open(src) + if err != nil { + log.Printf("Could not open %s file\n", src) + return err + } + defer in.Close() + + _, dstMountPoint := GetVolumeMountPoints(os.Getenv(bootstrapVolume)) + dst := dstMountPoint + "/" + bootstrapHelpFile + out, err := os.Create(dst) + if err != nil { + log.Printf("Could not create %s file\n", dst) + return err + } + defer out.Close() + + _, err = io.Copy(out, in) + if err != nil { + log.Printf("Failed to copy %s file to %s\n", src, dst) + return err + } + return out.Close() +} diff --git a/bootstrap_capz/config/azure_config.go b/bootstrap_capz/config/azure_config.go new file mode 100644 index 0000000..d5dee0d --- /dev/null +++ b/bootstrap_capz/config/azure_config.go @@ -0,0 +1,137 @@ +/* +Copyright 2014 The Kubernetes 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 config + +import ( + "errors" + "io/ioutil" + "log" + + "gopkg.in/go-playground/validator.v9" + "sigs.k8s.io/yaml" +) + +// AzureConfig holds configurations for bootstrap steps +type AzureConfig struct { + // +optional + Kind string `json:"kind" validate:"required"` + + // +optional + APIVersion string `json:"apiVersion" validate:"required"` + + // Configuration parameters for metadata + Metadata *Metadata `json:"metadata"` + + // Configuration parameters for metadata + Credentials *Credentials `json:"credentials" validate:"required"` + + // Configuration parameters for spec + Spec *Spec `json:"spec"` +} + +// Metadata structure provides the cluster name to assign and labels to the k8s cluster +type Metadata struct { + Name string `json:"name" validate:"required"` + Labels []string `json:"labels,omitempty"` +} + +// Credentials structu provides the credentials to authenticate with Azure Cloud +type Credentials struct { + Tenant string `json:"tenant" validate:"required"` + Client string `json:"client" validate:"required"` + Secret string `json:"secret" validate:"required"` +} + +// Spec structure contains the info for the ck8s luster to deploy +type Spec struct { + Region string `json:"region,omitempty"` + ResourceGroup string `json:"resourceGroup,omitempty"` + Cluster Cluster `json:"cluster"` +} + +// Cluster struct provides data for the k8s cluster to deploy +type Cluster struct { + // Kubernetes version to deploy + K8SVersion string `json:"k8sVersion,omitempty"` + + // Azure VM size to use for the cluster + VMSize string `json:"vmSize,omitempty"` + + // Number of nodes to deploy for the cluster + Replicas uint8 `json:"replicas,omitempty" validate:"gte=1,lte=100"` + + // Kubeconfig filename to save + Kubeconfig string `json:"kubeconfig,omitempty"` +} + +// ReadYAMLFile reads YAML-formatted configuration file and +// de-serializes it to a given object +func ReadYAMLFile(filePath string, cfg *AzureConfig) error { + data, err := ioutil.ReadFile(filePath) + if err != nil { + log.Printf("yamlFile.Get err #%v ", err) + return err + } + return yaml.Unmarshal(data, cfg) +} + +// ReadYAMLtoJSON reads YAML-formatted configuration file and +// de-serializes it to a given object +func ReadYAMLtoJSON(filePath string) (string, error) { + data, err := ioutil.ReadFile(filePath) + if err != nil { + log.Printf("Failed to read Azure Ephemeral configuration file: err #%v ", err) + + return "", err + } + jsonByte, err := yaml.YAMLToJSON(data) + if err != nil { + log.Printf("YAMLtoJSON err #%v ", err) + return "", err + } + jsonStr := string(jsonByte) + return jsonStr, nil +} + +// ValidateConfigFile validates Azure configuration file for the Ephemeral Cluster +func ValidateConfigFile(config *AzureConfig) error { + var validate *validator.Validate + + validate = validator.New() + err := validate.Struct(config) + if err != nil { + // this check is only needed when your code could produce + // an invalid value for validation such as interface with nil value. + var invalidError *validator.InvalidValidationError + if errors.As(err, &invalidError) { + log.Println(err) + return err + } + + log.Printf("Ephemeral cluster configuration file validation failed") + for _, err := range err.(validator.ValidationErrors) { + log.Printf(" Namespace = %s\n", err.Namespace()) + log.Printf(" Tag = %s\n", err.Tag()) + log.Printf(" Type = %s\n", err.Type()) + log.Printf(" Value = %s\n", err.Value()) + log.Printf(" Param = %s\n", err.Param()) + log.Println() + } + return err + } + return nil +} diff --git a/bootstrap_capz/main.go b/bootstrap_capz/main.go new file mode 100644 index 0000000..84c884e --- /dev/null +++ b/bootstrap_capz/main.go @@ -0,0 +1,78 @@ +/* + 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 ( + "flag" + "log" + "os" + + "opendev.org/airship/images/bootstrap_capz/config" +) + +const ( + createCmd = "create" + deleteCmd = "delete" + helpCmd = "help" +) + +func main() { + var configPath string + + flag.StringVar(&configPath, "c", "", "Path for the Azure bootstrap configuration (yaml) file") + flag.Parse() + + if configPath == "" { + volMount := os.Getenv("BOOTSTRAP_VOLUME") + _, dstMount := config.GetVolumeMountPoints(volMount) + azureConfigPath := dstMount + "/" + os.Getenv("BOOTSTRAP_CONFIG") + configPath = azureConfigPath + } + + configYAML := &config.AzureConfig{} + err := config.ReadYAMLFile(configPath, configYAML) + if err != nil { + log.Printf("Failed to load Azure Bootstrap config file") + os.Exit(1) + } + + err = config.ValidateConfigFile(configYAML) + if err != nil { + os.Exit(2) + } + + command := os.Getenv("BOOTSTRAP_COMMAND") + switch { + case command == createCmd: + err = config.CreateAKSCluster(configYAML) + if err != nil { + os.Exit(5) + } + case command == deleteCmd: + err = config.DeleteAKSCluster(configYAML) + if err != nil { + os.Exit(6) + } + case command == helpCmd: + err = config.HelpAKSCluster() + if err != nil { + os.Exit(7) + } + default: + log.Printf("The --command parameter value shall be 'create', 'delete' or 'help'") + os.Exit(8) + } + os.Exit(0) +}