image-builder integration for ISO builds

This introduces airshipctl integration with image-builder [0], which
replaces the existing isogen tool for ephemeral ISO generation.

The airshipctl isogen executor has been updated for building ephemeral
ISOs using the image-builder container. The ability for user-declared
filenames for cloud-init user data and network data was removed, since
the user's only interest is in supplying the relevant overrides, not in
transparent naming coordination with the image-builder container. A new
object is added to the document package to identify the document kind,
label, and key to retrieve data from since this is pattern we will
reuse elsewhere.

Progress flag removed as requsted. Progress is reported directly by the
image-builder container.

Isogen debug flag removed in favor of using log.DebugEnabled()

[0] https://review.opendev.org/#/c/730777/

Depends-On: https://review.opendev.org/c/airship/images/+/730777/
Change-Id: I545004feaf2116f8ffb29faf6f7f7f5fcfe24fff
changes/82/758782/69
Anderson, Craig (ca846m) 3 years ago
parent dbb006c02d
commit 2daacf5f2a

@ -7,4 +7,4 @@ Flags:
-h, --help help for ejectmedia
-l, --labels string Label(s) to filter desired baremetal host documents
-n, --name string Name to filter desired baremetal host document
--phase string airshipctl phase that contains the desired baremetal host document(s) (default "bootstrap")
--phase string airshipctl phase that contains the desired baremetal host document(s) (default "bootstrap-iso")

@ -7,4 +7,4 @@ Flags:
-h, --help help for poweroff
-l, --labels string Label(s) to filter desired baremetal host documents
-n, --name string Name to filter desired baremetal host document
--phase string airshipctl phase that contains the desired baremetal host document(s) (default "bootstrap")
--phase string airshipctl phase that contains the desired baremetal host document(s) (default "bootstrap-iso")

@ -7,4 +7,4 @@ Flags:
-h, --help help for poweron
-l, --labels string Label(s) to filter desired baremetal host documents
-n, --name string Name to filter desired baremetal host document
--phase string airshipctl phase that contains the desired baremetal host document(s) (default "bootstrap")
--phase string airshipctl phase that contains the desired baremetal host document(s) (default "bootstrap-iso")

@ -7,4 +7,4 @@ Flags:
-h, --help help for powerstatus
-l, --labels string Label(s) to filter desired baremetal host documents
-n, --name string Name to filter desired baremetal host document
--phase string airshipctl phase that contains the desired baremetal host document(s) (default "bootstrap")
--phase string airshipctl phase that contains the desired baremetal host document(s) (default "bootstrap-iso")

@ -7,4 +7,4 @@ Flags:
-h, --help help for reboot
-l, --labels string Label(s) to filter desired baremetal host documents
-n, --name string Name to filter desired baremetal host document
--phase string airshipctl phase that contains the desired baremetal host document(s) (default "bootstrap")
--phase string airshipctl phase that contains the desired baremetal host document(s) (default "bootstrap-iso")

@ -21,28 +21,19 @@ import (
"opendev.org/airship/airshipctl/pkg/phase"
)
// NewImageBuildCommand creates a new command with the capability to build an ISO image.
// NewImageBuildCommand creates a new command with the capability to build ISO image.
func NewImageBuildCommand(cfgFactory config.Factory) *cobra.Command {
var progress bool
cmd := &cobra.Command{
Use: "build",
Short: "Build ISO image",
RunE: func(cmd *cobra.Command, args []string) error {
p := &phase.RunCommand{
Factory: cfgFactory,
Options: phase.RunFlags{Progress: progress},
}
p.Options.PhaseID.Name = config.BootstrapPhase
return p.RunE()
},
}
flags := cmd.Flags()
flags.BoolVar(
&progress,
"progress",
false,
"show progress")
return cmd
}

@ -12,28 +12,28 @@ In a nutshell, users of `airshipctl` should be able to do the following:
1. Create an `airshipctl` Airship Configuration for their site - sort of like a
kubeconfig file. Airshipctl can create a pre-configured config file by
running `airshipctl config init`.
2. Create a set of declarative documents representing the infrastructure
1. Create a set of declarative documents representing the infrastructure
(baremetal, cloud) and software.
3. Run `airshipctl document pull` to clone the document repositories in your
1. Run `airshipctl document pull` to clone the document repositories in your
Airship Configuration.
4. When deploying against baremetal infrastructure, run
`airshipctl baremetal isogen` to generate a self-contained ISO that can be
used to boot the first host in the cluster into an ephemeral Kubernetes node.
5. When deploying against baremetal infrastructure, run
1. Run `airshipctl image build` to generate a self-contained ISO
that can be used to boot the first host in the cluster into an ephemeral
Kubernetes node.
1. When deploying against baremetal infrastructure, run
`airshipctl baremetal remotedirect` to remotely provision the first machine
in the cluster using the generated ISO, providing an ephemeral Kubernetes
instance that `airshipctl` can communicate with for subsequent steps. This
ephemeral host provides a foothold in the target environment so we can follow
the standard cluster-api bootstrap flow.
6. Run `airshipctl phase run initinfra-ephemeral` to bootstrap the new ephemeral cluster
1. Run `airshipctl phase run initinfra-ephemeral` to bootstrap the new ephemeral cluster
with enough of the chosen cluster-api provider components to provision the
target cluster.
7. Run `airshipctl clusterctl` to use the ephemeral Kubernetes host to provision
1. Run `airshipctl clusterctl` to use the ephemeral Kubernetes host to provision
at least one node of the target cluster using the cluster-api bootstrap flow.
8. Run `airshipctl cluster initinfra --clustertype=target` to bootstrap the new
1. Run `airshipctl cluster initinfra --clustertype=target` to bootstrap the new
target cluster with any remaining infrastructure necessary to begin running
more complex workflows such as Argo.
9. Run `airshipctl workflow submit sitemanage` to run the out of the box sitemanage
1. Run `airshipctl workflow submit sitemanage` to run the out of the box sitemanage
workflow, which will leverage Argo to handle bootstrapping the remaining
infrastructure as well as deploying and/or updating software.

@ -16,7 +16,7 @@ airshipctl baremetal ejectmedia [flags]
-h, --help help for ejectmedia
-l, --labels string Label(s) to filter desired baremetal host documents
-n, --name string Name to filter desired baremetal host document
--phase string airshipctl phase that contains the desired baremetal host document(s) (default "bootstrap")
--phase string airshipctl phase that contains the desired baremetal host document(s) (default "bootstrap-iso")
```
### Options inherited from parent commands

@ -16,7 +16,7 @@ airshipctl baremetal poweroff [flags]
-h, --help help for poweroff
-l, --labels string Label(s) to filter desired baremetal host documents
-n, --name string Name to filter desired baremetal host document
--phase string airshipctl phase that contains the desired baremetal host document(s) (default "bootstrap")
--phase string airshipctl phase that contains the desired baremetal host document(s) (default "bootstrap-iso")
```
### Options inherited from parent commands

@ -16,7 +16,7 @@ airshipctl baremetal poweron [flags]
-h, --help help for poweron
-l, --labels string Label(s) to filter desired baremetal host documents
-n, --name string Name to filter desired baremetal host document
--phase string airshipctl phase that contains the desired baremetal host document(s) (default "bootstrap")
--phase string airshipctl phase that contains the desired baremetal host document(s) (default "bootstrap-iso")
```
### Options inherited from parent commands

@ -16,7 +16,7 @@ airshipctl baremetal powerstatus [flags]
-h, --help help for powerstatus
-l, --labels string Label(s) to filter desired baremetal host documents
-n, --name string Name to filter desired baremetal host document
--phase string airshipctl phase that contains the desired baremetal host document(s) (default "bootstrap")
--phase string airshipctl phase that contains the desired baremetal host document(s) (default "bootstrap-iso")
```
### Options inherited from parent commands

@ -16,7 +16,7 @@ airshipctl baremetal reboot [flags]
-h, --help help for reboot
-l, --labels string Label(s) to filter desired baremetal host documents
-n, --name string Name to filter desired baremetal host document
--phase string airshipctl phase that contains the desired baremetal host document(s) (default "bootstrap")
--phase string airshipctl phase that contains the desired baremetal host document(s) (default "bootstrap-iso")
```
### Options inherited from parent commands

@ -14,7 +14,6 @@ airshipctl image build [flags]
```
-h, --help help for build
--progress show progress
```
### Options inherited from parent commands

@ -9,7 +9,6 @@ require (
github.com/Masterminds/sprig v2.22.0+incompatible
github.com/Microsoft/go-winio v0.4.14 // indirect
github.com/chai2010/gettext-go v0.0.0-20170215093142-bf70f2a70fb1 // indirect
github.com/cheggaaa/pb/v3 v3.0.4
github.com/containerd/containerd v1.4.1 // indirect
github.com/docker/docker v1.4.2-0.20200203170920-46ec8731fbce
github.com/docker/go-connections v0.4.0 // indirect

@ -65,8 +65,6 @@ github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdko
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
github.com/VividCortex/ewma v1.1.1 h1:MnEK4VOv6n0RSY4vtRe3h11qjxL3+t0B8yOL8iMXdcM=
github.com/VividCortex/ewma v1.1.1/go.mod h1:2Tkkvm3sRDVXaiyucHiACn4cqf7DpdyLvmxzcbUokwA=
github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs=
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
@ -108,8 +106,6 @@ github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5/go.mod h1:/iP1
github.com/chai2010/gettext-go v0.0.0-20170215093142-bf70f2a70fb1 h1:HD4PLRzjuCVW79mQ0/pdsalOLHJ+FaEoqJLxfltpb2U=
github.com/chai2010/gettext-go v0.0.0-20170215093142-bf70f2a70fb1/go.mod h1:/iP1qXHoty45bqomnu2LM+VVyAEdWN+vtSHGlQgyxbw=
github.com/cheekybits/genny v0.0.0-20170328200008-9127e812e1e9/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
github.com/cheggaaa/pb/v3 v3.0.4 h1:QZEPYOj2ix6d5oEg63fbHmpolrnNiwjUsk+h74Yt4bM=
github.com/cheggaaa/pb/v3 v3.0.4/go.mod h1:7rgWxLrAUcFMkvJuv09+DYi7mMUYi8nO9iOWcvGJPfw=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
@ -489,18 +485,13 @@ github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7
github.com/marten-seemann/qtls v0.2.3/go.mod h1:xzjG7avBwGGbdZ8dTGxlBnLArsVKLvwmjgmPuiQEcYk=
github.com/matoous/godox v0.0.0-20190911065817-5d6d842e92eb/go.mod h1:1BELzlh859Sh1c6+90blK8lbYy0kwQf1bYlBhBysy1s=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54=
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
@ -842,9 +833,7 @@ golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191002063906-3421d5a6bb1c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191128015809-6d18c012aee9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

@ -1,14 +1,20 @@
apiVersion: airshipit.org/v1alpha1
kind: ImageConfiguration
kind: IsoConfiguration
metadata:
name: isogen
labels:
airshipit.org/deploy-k8s: "false"
builder:
networkConfigFileName: network-config
outputMetadataFileName: output-metadata.yaml
userDataFileName: user-data
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:
containerRuntime: docker
image: quay.io/airshipit/isogen:latest-ubuntu_focal
volume: /srv/iso:/config
image: quay.io/airshipit/image-builder:latest-ubuntu_focal
volume: /srv/images:/config

@ -4,4 +4,4 @@ metadata:
name: default
labels:
airshipit.org/deploy-k8s: "false"
isoUrl: http://localhost:8099/ubuntu-focal.iso
isoUrl: http://localhost:8099/ephemeral.iso

@ -12,6 +12,8 @@ stringData:
# TODO: add download sources to the versions catalogue
userData: |
#cloud-config
# Expect that packages are already installed in base image
package_update: false
ssh_pwauth: True
chpasswd:
list: |
@ -24,32 +26,7 @@ stringData:
gecos: deployer
ssh_pwauth: True
runcmd:
- |
cat <<EOF | tee /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-iptables = 1
EOF
- sysctl --system
- swapoff -a
- export HTTP_PROXY=REPLACEMENT_HTTP_PROXY
- export HTTPS_PROXY=REPLACEMENT_HTTPS_PROXY
- export http_proxy=${HTTP_PROXY}
- export https_proxy=${HTTPS_PROXY}
- export NO_PROXY=REPLACEMENT_NO_PROXY
- export no_proxy=${NO_PROXY}
- curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add -
- curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -
- echo "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | tee -a /etc/apt/sources.list
# Replace xenial with focal or $(lsb_release -cs) once available
- echo "deb https://apt.kubernetes.io/ kubernetes-xenial main" | tee -a /etc/apt/sources.list
- apt update
- apt install -y
docker-ce="$(apt-cache policy docker-ce | grep 19.03.12 | sort | head -n 1 | tr -s " " | cut -d ' ' -f 2)"
docker-ce-cli="$(apt-cache policy docker-ce-cli | grep 19.03.12 | sort | head -n 1 | tr -s " " | cut -d ' ' -f 2)"
containerd.io
- apt install -y kubelet=1.18.6-00 kubeadm=1.18.6-00 kubectl=1.18.6-00
- apt-mark hold docker-ce docker-ce-cli containerd.io kubelet kubeadm kubectl
- unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY no_proxy NO_PROXY
- /bin/bash -c 'kernel_libsubdir="$(ls /lib/modules | head -1)"; config_dir="/lib/modules/${kernel_libsubdir}/build"; mkdir -p "${config_dir}"; if [ -f /run/live/medium/config ] && [ ! -f "${config_dir}/.config" ]; then ln -s /run/live/medium/config "${config_dir}/.config"; fi;'
- kubeadm init --config /tmp/kubeadm.yaml
- mkdir -p /opt/metal3-dev-env/ironic/html/images
write_files:

@ -19,19 +19,17 @@ move-options: {}
action: move
---
apiVersion: airshipit.org/v1alpha1
kind: ImageConfiguration
kind: IsoConfiguration
metadata:
name: isogen
labels:
airshipit.org/deploy-k8s: "false"
builder:
networkConfigFileName: network-config
outputMetadataFileName: output-metadata.yaml
userDataFileName: user-data
outputFileName: ephemeral.iso
container:
containerRuntime: docker
image: quay.io/airshipit/isogen:latest-ubuntu_focal
volume: /srv/iso:/config
image: quay.io/airshipit/image-builder:latest-ubuntu_focal
volume: /srv/images:/config
---
apiVersion: airshipit.org/v1alpha1
kind: GenericContainer

@ -1,11 +1,11 @@
apiVersion: airshipit.org/v1alpha1
kind: Phase
metadata:
name: bootstrap
name: bootstrap-iso
config:
executorRef:
apiVersion: airshipit.org/v1alpha1
kind: ImageConfiguration
kind: IsoConfiguration
name: isogen
documentEntryPoint: ephemeral/bootstrap
---

@ -46,7 +46,7 @@ func init() {
&PhasePlan{},
&KubeConfig{},
&KubernetesApply{},
&ImageConfiguration{},
&IsoConfiguration{},
&RemoteDirectConfiguration{},
&ClusterMap{},
&ReplacementTransformer{},

@ -1,59 +0,0 @@
/*
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 v1alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// Container structure contains parameters related to Docker runtime, used for building image
type Container struct {
// Container volume directory binding.
Volume string `json:"volume,omitempty"`
// ISO generator container image URL
Image string `json:"image,omitempty"`
// Container Runtime Interface driver
ContainerRuntime string `json:"containerRuntime,omitempty"`
}
// Builder structure defines metadata files (including Cloud Init metadata) used for image
type Builder struct {
// Cloud Init user-data file name placed to the container volume root
UserDataFileName string `json:"userDataFileName,omitempty"`
// Cloud Init network-config file name placed to the container volume root
NetworkConfigFileName string `json:"networkConfigFileName,omitempty"`
// File name for output metadata
OutputMetadataFileName string `json:"outputMetadataFileName,omitempty"`
}
// +kubebuilder:object:root=true
// ImageConfiguration structure is inherited from apimachinery TypeMeta and ObjectMeta and is a top level
// configuration structure for building image
type ImageConfiguration struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Container *Container `json:"container,omitempty"`
Builder *Builder `json:"builder,omitempty"`
}
// DefaultImageConfiguration can be used to safely unmarshal ImageConfiguration object without nil pointers
func DefaultImageConfiguration() *ImageConfiguration {
return &ImageConfiguration{
Container: &Container{},
Builder: &Builder{},
}
}

@ -0,0 +1,64 @@
/*
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 v1alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/kustomize/api/types"
)
// IsoContainer structure contains parameters related to Docker runtime, used for building image
type IsoContainer struct {
// Container volume directory binding.
Volume string `json:"volume,omitempty"`
// ISO generator container image URL
Image string `json:"image,omitempty"`
// Container Runtime Interface driver
ContainerRuntime string `json:"containerRuntime,omitempty"`
}
// Isogen structure defines document selection criteria for cloud-init metadata
type Isogen struct {
// Cloud Init user data will be retrieved from the doc matching this object
UserDataSelector types.Selector `json:"userDataSelector,omitempty"`
// Cloud init user data will be retrieved from this document key
UserDataKey string `jsong:"userDataKey,omitempty"`
// Cloud Init network config will be retrieved from the doc matching this object
NetworkConfigSelector types.Selector `json:"networkConfigSelector,omitempty"`
// Cloud init network config will be retrieved from this document key
NetworkConfigKey string `jsong:"networkConfigKey,omitempty"`
// File name to use for the output image that will be written to the container volume root
OutputFileName string `json:"outputFileName,omitempty"`
}
// +kubebuilder:object:root=true
// IsoConfiguration structure is inherited from apimachinery TypeMeta and ObjectMeta and is a top level
// configuration structure for building image
type IsoConfiguration struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
IsoContainer *IsoContainer `json:"container,omitempty"`
Isogen *Isogen `json:"builder,omitempty"`
}
// DefaultIsoConfiguration can be used to safely unmarshal IsoConfiguration object without nil pointers
func DefaultIsoConfiguration() *IsoConfiguration {
return &IsoConfiguration{
IsoContainer: &IsoContainer{},
Isogen: &Isogen{},
}
}

@ -112,21 +112,6 @@ func (in *BootstrapContainer) DeepCopy() *BootstrapContainer {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Builder) DeepCopyInto(out *Builder) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Builder.
func (in *Builder) DeepCopy() *Builder {
if in == nil {
return nil
}
out := new(Builder)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Cluster) DeepCopyInto(out *Cluster) {
*out = *in
@ -258,21 +243,6 @@ func (in *Clusterctl) DeepCopyObject() runtime.Object {
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Container) DeepCopyInto(out *Container) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Container.
func (in *Container) DeepCopy() *Container {
if in == nil {
return nil
}
out := new(Container)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *EphemeralCluster) DeepCopyInto(out *EphemeralCluster) {
*out = *in
@ -306,41 +276,6 @@ func (in *GenericContainer) DeepCopyObject() runtime.Object {
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ImageConfiguration) DeepCopyInto(out *ImageConfiguration) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
if in.Container != nil {
in, out := &in.Container, &out.Container
*out = new(Container)
**out = **in
}
if in.Builder != nil {
in, out := &in.Builder, &out.Builder
*out = new(Builder)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImageConfiguration.
func (in *ImageConfiguration) DeepCopy() *ImageConfiguration {
if in == nil {
return nil
}
out := new(ImageConfiguration)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *ImageConfiguration) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ImageMeta) DeepCopyInto(out *ImageMeta) {
*out = *in
@ -391,6 +326,73 @@ func (in *InitOptions) DeepCopy() *InitOptions {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *IsoConfiguration) DeepCopyInto(out *IsoConfiguration) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
if in.IsoContainer != nil {
in, out := &in.IsoContainer, &out.IsoContainer
*out = new(IsoContainer)
**out = **in
}
if in.Isogen != nil {
in, out := &in.Isogen, &out.Isogen
*out = new(Isogen)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IsoConfiguration.
func (in *IsoConfiguration) DeepCopy() *IsoConfiguration {
if in == nil {
return nil
}
out := new(IsoConfiguration)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *IsoConfiguration) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *IsoContainer) DeepCopyInto(out *IsoContainer) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IsoContainer.
func (in *IsoContainer) DeepCopy() *IsoContainer {
if in == nil {
return nil
}
out := new(IsoContainer)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Isogen) DeepCopyInto(out *Isogen) {
*out = *in
out.UserDataSelector = in.UserDataSelector
out.NetworkConfigSelector = in.NetworkConfigSelector
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Isogen.
func (in *Isogen) DeepCopy() *Isogen {
if in == nil {
return nil
}
out := new(Isogen)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *KubeConfig) DeepCopyInto(out *KubeConfig) {
*out = *in

@ -15,27 +15,54 @@
package cloudinit
import (
b64 "encoding/base64"
"opendev.org/airship/airshipctl/pkg/document"
"sigs.k8s.io/kustomize/api/resid"
"sigs.k8s.io/kustomize/api/types"
)
const (
networkDataKey = "networkData"
userDataKey = "userData"
var (
// Initialize defaults where we expect to find user-data and
// network config data in manifests
userDataSelectorDefaults = types.Selector{
Gvk: resid.Gvk{Kind: document.SecretKind},
LabelSelector: document.EphemeralUserDataSelector,
}
userDataKeyDefault = "userData"
networkConfigSelectorDefaults = types.Selector{
Gvk: resid.Gvk{Kind: document.BareMetalHostKind},
LabelSelector: document.EphemeralHostSelector,
}
networkConfigKeyDefault = "networkData"
)
// GetCloudData reads YAML document input and generates cloud-init data for
// ephemeral node.
func GetCloudData(docBundle document.Bundle) (userData []byte, netConf []byte, err error) {
userData, err = getUserData(docBundle)
func GetCloudData(
docBundle document.Bundle,
userDataSelector types.Selector,
userDataKey string,
networkConfigSelector types.Selector,
networkConfigKey string,
) (userData []byte, netConf []byte, err error) {
userDataSelectorFinal, userDataKeyFinal := applyDefaultsAndGetData(
userDataSelector,
userDataSelectorDefaults,
userDataKey,
userDataKeyDefault,
)
userData, err = document.GetSecretData(docBundle, userDataSelectorFinal, userDataKeyFinal)
if err != nil {
return nil, nil, err
}
netConf, err = getNetworkData(docBundle)
netConfSelectorFinal, netConfKeyFinal := applyDefaultsAndGetData(
networkConfigSelector,
networkConfigSelectorDefaults,
networkConfigKey,
networkConfigKeyDefault,
)
netConf, err = getNetworkData(docBundle, netConfSelectorFinal, netConfKeyFinal)
if err != nil {
return nil, nil, err
}
@ -43,26 +70,36 @@ func GetCloudData(docBundle document.Bundle) (userData []byte, netConf []byte, e
return userData, netConf, err
}
func getUserData(docBundle document.Bundle) ([]byte, error) {
// find the user-data document
selector := document.NewEphemeralCloudDataSelector()
userDataDoc, err := docBundle.SelectOne(selector)
if err != nil {
return nil, err
func applyDefaultsAndGetData(
docSelector types.Selector,
docSelectorDefaults types.Selector,
key string,
keyDefault string,
) (types.Selector, string) {
// Assign defaults if there are no user supplied overrides
if docSelector.Kind == "" &&
docSelector.Name == "" &&
docSelector.AnnotationSelector == "" &&
docSelector.LabelSelector == "" {
docSelector.Kind = docSelectorDefaults.Kind
docSelector.LabelSelector = docSelectorDefaults.LabelSelector
}
// finally, try and retrieve the data we want from the document
userData, err := decodeData(userDataDoc, userDataKey)
if err != nil {
return nil, err
keyFinal := key
if key == "" {
keyFinal = keyDefault
}
return userData, nil
return docSelector, keyFinal
}
func getNetworkData(docBundle document.Bundle) ([]byte, error) {
func getNetworkData(
docBundle document.Bundle,
netCfgSelector types.Selector,
netCfgKey string,
) ([]byte, error) {
// find the baremetal host indicated as the ephemeral node
selector := document.NewEphemeralBMHSelector()
selector := document.NewSelector().ByKind(netCfgSelector.Kind).ByLabel(netCfgSelector.LabelSelector)
d, err := docBundle.SelectOne(selector)
if err != nil {
return nil, err
@ -80,37 +117,10 @@ func getNetworkData(docBundle document.Bundle) ([]byte, error) {
}
// finally, try and retrieve the data we want from the document
netData, err := decodeData(d, networkDataKey)
netData, err := document.DecodeSecretData(d, netCfgKey)
if err != nil {
return nil, err
}
return netData, nil
}
func decodeData(cfg document.Document, key string) ([]byte, error) {
var needsBase64Decode = false
// TODO(alanmeadows): distinguish between missing net-data key
// and missing data/stringData keys in the Secret
data, err := cfg.GetStringMap("data")
if err == nil {
needsBase64Decode = true
} else {
// we'll catch any error below
data, err = cfg.GetStringMap("stringData")
if err != nil {
return nil, ErrDataNotSupplied{DocName: cfg.GetName(), Key: "data or stringData"}
}
}
res, ok := data[key]
if !ok {
return nil, ErrDataNotSupplied{DocName: cfg.GetName(), Key: key}
}
if needsBase64Decode {
return b64.StdEncoding.DecodeString(res)
}
return []byte(res), nil
}

@ -2,9 +2,7 @@
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.
@ -21,6 +19,9 @@ import (
"github.com/stretchr/testify/require"
"opendev.org/airship/airshipctl/pkg/document"
"sigs.k8s.io/kustomize/api/resid"
"sigs.k8s.io/kustomize/api/types"
)
func TestGetCloudData(t *testing.T) {
@ -28,19 +29,35 @@ func TestGetCloudData(t *testing.T) {
require.NoError(t, err, "Building Bundle Failed")
tests := []struct {
labelFilter string
expectedUserData []byte
expectedNetData []byte
expectedErr error
labelFilter string
userDataSelector types.Selector
userDataKey string
networkConfigSelector types.Selector
networkConfigKey string
expectedUserData []byte
expectedNetData []byte
expectedErr error
}{
{
labelFilter: "test=validdocset",
expectedUserData: []byte("cloud-init"),
expectedNetData: []byte("net-config"),
expectedErr: nil,
labelFilter: "test=validdocset",
userDataSelector: types.Selector{},
networkConfigSelector: types.Selector{},
expectedUserData: []byte("cloud-init"),
expectedNetData: []byte("net-config"),
expectedErr: nil,
},
{
labelFilter: "test=ephemeralmissing",
labelFilter: "test=ephemeralmissing",
userDataSelector: types.Selector{
Gvk: resid.Gvk{Kind: "Secret"},
LabelSelector: "airshipit.org/ephemeral-user-data in (True, true)",
},
userDataKey: "userData",
networkConfigSelector: types.Selector{
Gvk: resid.Gvk{Kind: "BareMetalHost"},
LabelSelector: "airshipit.org/ephemeral-node in (True, true)",
},
networkConfigKey: "networkData",
expectedUserData: nil,
expectedNetData: nil,
expectedErr: document.ErrDocNotFound{
@ -50,7 +67,17 @@ func TestGetCloudData(t *testing.T) {
},
},
{
labelFilter: "test=ephemeralduplicate",
labelFilter: "test=ephemeralduplicate",
userDataSelector: types.Selector{
Gvk: resid.Gvk{Kind: "Secret"},
LabelSelector: "airshipit.org/ephemeral-user-data in (True, true)",
},
userDataKey: "userData",
networkConfigSelector: types.Selector{
Gvk: resid.Gvk{Kind: "BareMetalHost"},
LabelSelector: "airshipit.org/ephemeral-node in (True, true)",
},
networkConfigKey: "networkData",
expectedUserData: nil,
expectedNetData: nil,
expectedErr: document.ErrMultiDocsFound{
@ -60,7 +87,17 @@ func TestGetCloudData(t *testing.T) {
},
},
{
labelFilter: "test=networkdatabadpointer",
labelFilter: "test=networkdatabadpointer",
userDataSelector: types.Selector{
Gvk: resid.Gvk{Kind: "Secret"},
LabelSelector: "airshipit.org/ephemeral-user-data in (True, true)",
},
userDataKey: "userData",
networkConfigSelector: types.Selector{
Gvk: resid.Gvk{Kind: "BareMetalHost"},
LabelSelector: "airshipit.org/ephemeral-node in (True, true)",
},
networkConfigKey: "networkData",
expectedUserData: nil,
expectedNetData: nil,
expectedErr: document.ErrDocNotFound{
@ -71,19 +108,55 @@ func TestGetCloudData(t *testing.T) {
},
},
{
labelFilter: "test=networkdatamalformed",
labelFilter: "test=networkdatamalformed",
userDataSelector: types.Selector{
Gvk: resid.Gvk{Kind: "Secret"},
LabelSelector: "airshipit.org/ephemeral-user-data in (True, true)",
},
userDataKey: "userData",
networkConfigSelector: types.Selector{
Gvk: resid.Gvk{Kind: "BareMetalHost"},
LabelSelector: "airshipit.org/ephemeral-node in (True, true)",
},
networkConfigKey: "networkData",
expectedUserData: nil,
expectedNetData: nil,
expectedErr: ErrDataNotSupplied{DocName: "networkdatamalformed-malformed", Key: networkDataKey},
expectedErr: document.ErrDataNotSupplied{
DocName: "networkdatamalformed-malformed",
Key: networkConfigKeyDefault,
},
},
{
labelFilter: "test=userdatamalformed",
labelFilter: "test=userdatamalformed",
userDataSelector: types.Selector{
Gvk: resid.Gvk{Kind: "Secret"},
LabelSelector: "airshipit.org/ephemeral-user-data in (True, true)",
},
userDataKey: "userData",
networkConfigSelector: types.Selector{
Gvk: resid.Gvk{Kind: "BareMetalHost"},
LabelSelector: "airshipit.org/ephemeral-node in (True, true)",
},
networkConfigKey: "networkData",
expectedUserData: nil,
expectedNetData: nil,
expectedErr: ErrDataNotSupplied{DocName: "userdatamalformed-somesecret", Key: userDataKey},
expectedErr: document.ErrDataNotSupplied{
DocName: "userdatamalformed-somesecret",
Key: userDataKeyDefault,
},
},
{
labelFilter: "test=userdatamissing",
labelFilter: "test=userdatamissing",
userDataSelector: types.Selector{
Gvk: resid.Gvk{Kind: "Secret"},
LabelSelector: "airshipit.org/ephemeral-user-data in (True, true)",
},
userDataKey: "userData",
networkConfigSelector: types.Selector{
Gvk: resid.Gvk{Kind: "BareMetalHost"},
LabelSelector: "airshipit.org/ephemeral-node in (True, true)",
},
networkConfigKey: "networkData",
expectedUserData: nil,
expectedNetData: nil,
expectedErr: document.ErrDocNotFound{
@ -105,7 +178,13 @@ func TestGetCloudData(t *testing.T) {
require.NoError(t, err, "GetAllDocuments failed")
require.NotZero(t, docs)
actualUserData, actualNetData, actualErr := GetCloudData(filteredBundle)
actualUserData, actualNetData, actualErr := GetCloudData(
filteredBundle,
tt.userDataSelector,
tt.userDataKey,
tt.networkConfigSelector,
tt.networkConfigKey,
)
assert.Equal(t, tt.expectedUserData, actualUserData)
assert.Equal(t, tt.expectedNetData, actualNetData)

@ -1,41 +0,0 @@
/*
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 cloudinit
import (
"fmt"
)
// ErrDataNotSupplied error returned of no user-data or network configuration
// in the Secret
type ErrDataNotSupplied struct {
DocName string
Key string
}
// ErrDuplicateNetworkDataDocuments error returned if multiple network documents
// were found with the same name in the same namespace
type ErrDuplicateNetworkDataDocuments struct {
DocName string
Namespace string
}
func (e ErrDataNotSupplied) Error() string {
return fmt.Sprintf("Document %s has no key %s", e.DocName, e.Key)
}
func (e ErrDuplicateNetworkDataDocuments) Error() string {
return fmt.Sprintf("Found more than one document with the name %s in namespace %s", e.DocName, e.Namespace)
}

@ -15,17 +15,12 @@
package isogen
import (
"bufio"
"fmt"
"io"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"github.com/cheggaaa/pb/v3"
"opendev.org/airship/airshipctl/pkg/api/v1alpha1"
"opendev.org/airship/airshipctl/pkg/bootstrap/cloudinit"
"opendev.org/airship/airshipctl/pkg/config"
@ -36,19 +31,11 @@ import (
)
const (
builderConfigFileName = "builder-conf.yaml"
// progressBarTemplate is a template string for progress bar
// looks like 'Prefix [-->______] 20%' where Prefix is trimmed log line from docker container
progressBarTemplate = `{{string . "prefix"}} {{bar . }} {{percent . }} `
// defaultTerminalWidth is a default width of terminal if it's impossible to determine the actual one
defaultTerminalWidth = 80
// multiplier is a number of log lines produces while installing 1 package
multiplier = 3
// reInstallActions is a regular expression to check whether the log line contains of this substrings
reInstallActions = `Extracting|Unpacking|Configuring|Preparing|Setting`
reInstallBegin = `Retrieving Packages|newly installed`
reInstallFinish = `Base system installed successfully|mksquashfs`
builderConfigFileName = "builder-conf.yaml"
outputFileNameDefault = "ephemerial.iso"
userDataFileName = "user-data"
networkConfigFileName = "network-data"
outputMetadataFileName = "output-metadata.yaml"
)
// BootstrapIsoOptions are used to generate bootstrap ISO
@ -56,60 +43,63 @@ type BootstrapIsoOptions struct {
DocBundle document.Bundle
Builder container.Container
Doc document.Document
Cfg *v1alpha1.ImageConfiguration
Cfg *v1alpha1.IsoConfiguration
// optional fields for verbose output
Debug bool
Progress bool
Writer io.Writer
Writer io.Writer
}
// VerifyInputs verifies image configuration
func VerifyInputs(cfg *v1alpha1.ImageConfiguration) error {
if cfg.Container.Volume == "" {
func VerifyInputs(cfg *v1alpha1.IsoConfiguration) error {
if cfg.IsoContainer.Volume == "" {
return config.ErrMissingConfig{
What: "Must specify volume bind for ISO builder container",
}
}
if (cfg.Builder.UserDataFileName == "") || (cfg.Builder.NetworkConfigFileName == "") {
return config.ErrMissingConfig{
What: "UserDataFileName or NetworkConfigFileName are not specified in ISO builder config",
}
}
vols := strings.Split(cfg.Container.Volume, ":")
vols := strings.Split(cfg.IsoContainer.Volume, ":")
switch {
case len(vols) == 1:
cfg.Container.Volume = fmt.Sprintf("%s:%s", vols[0], vols[0])
cfg.IsoContainer.Volume = fmt.Sprintf("%s:%s", vols[0], vols[0])
case len(vols) > 2:
return config.ErrInvalidConfig{
What: "Bad container volume format. Use hostPath:contPath",
}
}
if cfg.Isogen.OutputFileName == "" {
log.Debugf("No outputFileName provided to Isogen. Using default: %s", outputFileNameDefault)
cfg.Isogen.OutputFileName = outputFileNameDefault
}
return nil
}
func getContainerCfg(
cfg *v1alpha1.ImageConfiguration,
func getIsoContainerCfg(
cfg *v1alpha1.IsoConfiguration,
builderCfgYaml []byte,
userData []byte,
netConf []byte,
) map[string][]byte {
hostVol := strings.Split(cfg.Container.Volume, ":")[0]
hostVol := strings.Split(cfg.IsoContainer.Volume, ":")[0]
fls := make(map[string][]byte)
fls[filepath.Join(hostVol, cfg.Builder.UserDataFileName)] = userData
fls[filepath.Join(hostVol, cfg.Builder.NetworkConfigFileName)] = netConf
fls[filepath.Join(hostVol, userDataFileName)] = userData
fls[filepath.Join(hostVol, networkConfigFileName)] = netConf
fls[filepath.Join(hostVol, builderConfigFileName)] = builderCfgYaml
return fls
}
// CreateBootstrapIso prepares and runs appropriate container to create a bootstrap ISO
func (opts BootstrapIsoOptions) CreateBootstrapIso() error {
cntVol := strings.Split(opts.Cfg.Container.Volume, ":")[1]
cntVol := strings.Split(opts.Cfg.IsoContainer.Volume, ":")[1]
log.Print("Creating cloud-init for ephemeral K8s")
userData, netConf, err := cloudinit.GetCloudData(opts.DocBundle)
userData, netConf, err := cloudinit.GetCloudData(
opts.DocBundle,
opts.Cfg.Isogen.UserDataSelector,
opts.Cfg.Isogen.UserDataKey,
opts.Cfg.Isogen.NetworkConfigSelector,
opts.Cfg.Isogen.NetworkConfigKey,
)
if err != nil {
return err
}
@ -119,21 +109,27 @@ func (opts BootstrapIsoOptions) CreateBootstrapIso() error {
return err
}
fls := getContainerCfg(opts.Cfg, builderCfgYaml, userData, netConf)
fls := getIsoContainerCfg(opts.Cfg, builderCfgYaml, userData, netConf)
if err = util.WriteFiles(fls, 0600); err != nil {
return err
}
vols := []string{opts.Cfg.Container.Volume}
vols := []string{opts.Cfg.IsoContainer.Volume}
builderCfgLocation := filepath.Join(cntVol, builderConfigFileName)
log.Printf("Running default container command. Mounted dir: %s", vols)
envVars := []string{
fmt.Sprintf("IMAGE_TYPE=iso"),
fmt.Sprintf("BUILDER_CONFIG=%s", builderCfgLocation),
fmt.Sprintf("USER_DATA_FILE=%s", userDataFileName),
fmt.Sprintf("NET_CONFIG_FILE=%s", networkConfigFileName),
fmt.Sprintf("OUTPUT_FILE_NAME=%s", opts.Cfg.Isogen.OutputFileName),
fmt.Sprintf("OUTPUT_METADATA_FILE_NAME=%s", outputMetadataFileName),
fmt.Sprintf("http_proxy=%s", os.Getenv("http_proxy")),
fmt.Sprintf("https_proxy=%s", os.Getenv("https_proxy")),
fmt.Sprintf("HTTP_PROXY=%s", os.Getenv("HTTP_PROXY")),
fmt.Sprintf("HTTPS_PROXY=%s", os.Getenv("HTTPS_PROXY")),
fmt.Sprintf("no_proxy=%s", os.Getenv("no_proxy")),
fmt.Sprintf("NO_PROXY=%s", os.Getenv("NO_PROXY")),
}
@ -144,25 +140,17 @@ func (opts BootstrapIsoOptions) CreateBootstrapIso() error {
log.Print("ISO generation is in progress. The whole process could take up to several minutes, please wait...")
if opts.Debug || opts.Progress {
if log.DebugEnabled() {
var cLogs io.ReadCloser
cLogs, err = opts.Builder.GetContainerLogs()
if err != nil {
log.Printf("failed to read container logs %s", err)
} else {
switch {
case opts.Progress:
if err = ShowProgress(cLogs, opts.Writer); err != nil {
log.Debugf("the following error occurred while showing progress bar: %s", err.Error())
}
case opts.Debug:
log.Print("start reading container logs")
// either container log output or progress bar will be shown
if _, err = io.Copy(opts.Writer, cLogs); err != nil {
log.Debugf("failed to write container logs to log output %s", err)
}
log.Print("got EOF from container logs")
log.Print("start reading container logs")
if _, err = io.Copy(opts.Writer, cLogs); err != nil {
log.Debugf("failed to write container logs to log output %s", err)
}
log.Print("got EOF from container logs")
}
}
@ -171,7 +159,7 @@ func (opts BootstrapIsoOptions) CreateBootstrapIso() error {
}
log.Print("ISO successfully built.")
if !opts.Debug {
if !log.DebugEnabled() {
log.Print("Removing container.")
return opts.Builder.RmContainer()
}
@ -179,140 +167,3 @@ func (opts BootstrapIsoOptions) CreateBootstrapIso() error {
log.Debugf("Debug flag is set. Container %s stopped but not deleted.", opts.Builder.GetID())
return nil
}
// ShowProgress prints progress bar during bootstrap ISO preparation
func ShowProgress(reader io.ReadCloser, writer io.Writer) error {
reFindActions := regexp.MustCompile(reInstallActions)
reBeginInstall := regexp.MustCompile(reInstallBegin)
reFinishInstall := regexp.MustCompile(reInstallFinish)
var bar *pb.ProgressBar
scanner := bufio.NewScanner(reader)
scanner.Split(bufio.ScanLines)
// Reading container log line by line
for scanner.Scan() {
curLine := scanner.Text()
// Trying to find entry points of package installation
switch {
case reBeginInstall.MatchString(curLine):
if err := finalizePb(bar, nil); err != nil {
return err
}
pkgCount, err := calculatePkgCount(scanner, writer, curLine)
if err != nil {
return finalizePb(bar, err)
}
bar, err = initPb(pkgCount, writer)
if err != nil {
return err
}
case reFinishInstall.MatchString(curLine):
if err := finalizePb(bar, nil); err != nil {
return err
}
case reFindActions.MatchString(curLine):
if err := incrementPb(bar, curLine); err != nil {
return finalizePb(bar, err)
}
case strings.Contains(curLine, "filesystem.squashfs"):
fmt.Fprintln(writer, curLine)
}
}
if bar != nil && bar.IsStarted() {
return finalizePb(bar, ErrUnexpectedPb{})
}
return nil
}
func finalizePb(bar *pb.ProgressBar, e error) error {
if bar != nil && bar.IsStarted() {
bar.SetCurrent(bar.Total())
if e != nil {
setPbPrefix(bar, "An error occurred while log parsing")
bar.Finish()
return e
}
setPbPrefix(bar, "Completed")
bar.Finish()
if err := bar.Err(); err != nil {
return err
}
}
return e
}
func initPb(pkgCount int, w io.Writer) (*pb.ProgressBar, error) {
bar := pb.ProgressBarTemplate(progressBarTemplate).New(pkgCount * multiplier)
bar.SetWriter(w).Start()
setPbPrefix(bar, "Installing required packages")
if err := bar.Err(); err != nil {
return nil, finalizePb(bar, err)
}
return bar, nil
}
func incrementPb(bar *pb.ProgressBar, curLine string) error {
if bar != nil && bar.IsStarted() && bar.Current() < bar.Total() {
setPbPrefix(bar, curLine)
bar.Increment()
if err := bar.Err(); err != nil {
return finalizePb(bar, err)
}
}
return nil
}
func setPbPrefix(bar *pb.ProgressBar, msg string) {
terminalWidth := defaultTerminalWidth
halfWidth := terminalWidth / 2
bar.SetWidth(terminalWidth)
if len(msg) > halfWidth {
msg = fmt.Sprintf("%v...", msg[0:halfWidth-3])
} else {
msg = fmt.Sprintf("%-*v", halfWidth, msg)
}
bar.Set("prefix", msg)
}
func calculatePkgCount(scanner *bufio.Scanner, writer io.Writer, curLine string) (int, error) {
reFindNumbers := regexp.MustCompile("[0-9]+")
// Trying to count how many packages is going to be installed
pkgCount := 0
matches := reFindNumbers.FindAllString(curLine, -1)
if matches == nil {
// There is no numbers in line about base packages, counting them manually to get estimates
fmt.Fprint(writer, "Retrieving base packages ")
for scanner.Scan() {
curLine = scanner.Text()
if strings.Contains(curLine, "Retrieving") {
pkgCount++
fmt.Fprint(writer, ".")
}
if strings.Contains(curLine, "Chosen extractor") {
fmt.Fprintln(writer, " Done")
return pkgCount, nil
}
}
}
if len(matches) >= 2 {
for _, v := range matches[0:2] {
j, err := strconv.Atoi(v)
if err != nil {
continue
}
pkgCount += j
}
if pkgCount > 0 {
return pkgCount, nil
}
}
return pkgCount, ErrNoParsedNumPkgs{}
}

@ -46,20 +46,19 @@ func TestBootstrapIso(t *testing.T) {
volBind := tempVol + ":/dst"
testErr := fmt.Errorf("TestErr")
testCfg := &api.ImageConfiguration{
Container: &api.Container{
testCfg := &api.IsoConfiguration{
IsoContainer: &api.IsoContainer{
Volume: volBind,
ContainerRuntime: "docker",
},
Builder: &api.Builder{
UserDataFileName: "user-data",