Bootstrap container for Azure

This commit provides the Go code and scripts for the Bootstrap container
for Azure.

The Bootstrap container (bootstrap_capz) for Azure is designed to accept
three commands: create, delete and help.
- create - will create an Ephemeral AKS cluster in Azure Cloud
- delete - will delete the Ephemeral AKS cluster from the Azure Cloud
- help - Stdout the help text for using this container.

Please, refer to the bootstrap_capz/README.md for further details.

Change-Id: Id1947f7b831a5d6cf59296cf39ff2b436080483d
This commit is contained in:
Sidney Shiba 2020-08-27 16:07:31 -05:00
parent aa624ce27c
commit 58f39ffa95
12 changed files with 746 additions and 0 deletions

3
.gitignore vendored
View File

@ -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

57
bootstrap_capz/Dockerfile Normal file
View File

@ -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"]

56
bootstrap_capz/Makefile Normal file
View File

@ -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)

32
bootstrap_capz/README.md Normal file
View File

@ -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.

View File

@ -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.
<YAML>
apiVersion: v1
kind: AzureConfig
metadata:
name: <Ephemeral K8S cluster name>
credentials:
tenant: <Azure Subscription Tenant ID>
client: <Azure Subscription Service Principal ID>
secret: <Azure Subscription Service Principal Secret>
spec:
resourceGroup: <Azure Resource Group Name>
region: <Azure Region, e.g., centralus>
cluster:
k8sVersion: <Kubernetes version, e.g., 1.18.6>
vmSize: <Azure Compute VM Type, e.g., Standard_B2s>
replicas: <Node Replica Number for the cluster. Default is 1>
kubeconfig: <Kubernetes version, e.g., 1.18.2>
</YAML>
It also accepts the JSON file format.
<JSON>
{
"apiVersion":"v1",
"kind":"AzureConfig",
"metadata":{
"name":"<Ephemeral K8S cluster name>"
},
"credentials":{
"tenant":"<Azure Subscription Tenant ID>",
"client":"<Azure Subscription Service Principal ID>",
"secret":"<Azure Subscription Service Principal Secret>"
},
"spec":{
"resourceGroup":"<Azure Resource Group Name>",
"region":"<Azure Region, e.g., centralus>",
"cluster":{
"k8sVersion":"<Kubernetes version, e.g., 1.18.6>",
"vmSize":"<Azure Compute VM Type, e.g., Standard_B2s>",
"replicas":<Node Replica Number for the cluster>,
"kubeconfig":"<Kubernetes version, e.g., 1.18.2>"
}
}
}
</JSON>
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:
<Snippet>
apiVersion: airshipit.org/v1alpha1
bootstrapInfo:
ephemeral:
container:
volume: /home/esidshi/.airship:/kube
</Snippet>

View File

@ -0,0 +1,22 @@
{
"apiVersion":"v1",
"credentials":{
"tenant":"<Your Azure Subscription Tenant ID>",
"client":"<Your Azure Service Principal ID>",
"secret":"<Your Azure Service Principal 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"
}
}
}

View File

@ -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: <Your Azure Subscription Tenant ID>
client: <Your Azure Service Principal ID>
secret: <Your Azure Service Principal Secret>
spec:
resourceGroup: airship2-zuul-rg
region: centralus
cluster:
k8sVersion: 1.18.6
vmSize: Standard_B2s
replicas: 1
kubeconfig: capz.kubeconfig

View File

@ -0,0 +1,3 @@
BOOTSTRAP_COMMAND=help
BOOTSTRAP_CONFIG=azure-config.yaml
BOOTSTRAP_VOLUME=/tmp:/kube

View File

@ -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
}

View File

@ -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()
}

View File

@ -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
}

78
bootstrap_capz/main.go Normal file
View File

@ -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)
}