Skip to content
This repository has been archived by the owner on Oct 27, 2023. It is now read-only.

Initial implementation to create single OCi image #4

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
163 changes: 78 additions & 85 deletions cmd/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ var createCmd = &cobra.Command{
},
}

// rpmOstreeClient creates a new rpm ostree client for the IBU imager
var rpmOstreeClient = NewClient("ibu-imager")

func init() {

// Add create command
Expand Down Expand Up @@ -87,12 +90,12 @@ func create() {

// Execute 'crictl ps -o json' command, parse the JSON output and extract image references using 'jq'
log.Debug("Save list of running containers")
criListContainers := fmt.Sprintf(`nsenter --target 1 --cgroup --mount --ipc --pid -- crictl images -o json | jq -r '.images[] | .repoDigests[], .repoTags[]' > ` + backupDir + `/containers.list`)
err = runCMD(criListContainers)
_, err = runInHostNamespace(
"crictl", append([]string{"images", "-o", "json", "|", "jq", "-r", "'.images[] | .repoDigests[], .repoTags[]'"}, ">", backupDir+"/containers.list")...)
check(err)

// Create the file /var/tmp/container_list.done
err = runCMD("touch /var/tmp/container_list.done")
_, err = os.Create("/var/tmp/container_list.done")
check(err)

log.Println("List of containers saved successfully.")
Expand All @@ -115,25 +118,26 @@ func create() {
log.Println("Stopping containers and CRI-O runtime.")

// Store current status of CRI-O systemd
crioService := fmt.Sprintf(`nsenter --target 1 --cgroup --mount --ipc --pid -- systemctl is-active crio > %s/crio.systemd.status`, backupDir)
_ = runCMD(crioService) // this commands returns 3 when crio is inactive
_, err = runInHostNamespace(
"systemctl", append([]string{"is-active", "crio"}, ">", backupDir+"/crio.systemd.status")...)
check(err)

// Read CRI-O systemd status from file
crioSystemdStatus, _ := readLineFromFile(backupDir + "/crio.systemd.status")

if crioSystemdStatus == "active" {

// CRI-O is active, so stop running containers
criStopContainers := fmt.Sprintf(`crictl ps -q | xargs --no-run-if-empty --max-args 1 --max-procs 10 crictl stop --timeout 5`)
log.Debug("Stop running containers")
err = runCMD(criStopContainers)
_, err = runInHostNamespace(
"crictl", []string{"ps", "-q", "|", "xargs", "--no-run-if-empty", "--max-args", "1", "--max-procs", "10", "crictl", "stop", "--timeout", "5"}...)
check(err)

// Waiting for containers to stop
waitCMD := fmt.Sprintf(`while crictl ps -q | grep -q . ; do sleep 1 ; done`)
log.Debug("Wait for containers to stop")
err = runCMD(waitCMD)
check(err)
// Waiting for containers to stop (TODO: implement this using runInHostNamespace)
//waitCMD := fmt.Sprintf(`while crictl ps -q | grep -q . ; do sleep 1 ; done`)
//log.Debug("Wait for containers to stop")
//err = runCMD(waitCMD)
//check(err)

// Execute a D-Bus call to stop the CRI-O runtime
log.Debug("Stopping CRI-O engine")
Expand All @@ -151,7 +155,8 @@ func create() {
log.Println("Create backup datadir")

// Check if the backup file for /var doesn't exist
if _, err := os.Stat(backupDir + "/var.tgz"); os.IsNotExist(err) {
varTarFile := backupDir + "/var.tgz"
if _, err = os.Stat(varTarFile); os.IsNotExist(err) {

// Define the 'exclude' patterns
excludePatterns := []string{
Expand All @@ -164,15 +169,15 @@ func create() {
}

// Build the tar command
args := []string{"czf", fmt.Sprintf("%s/var.tgz", backupDir)}
tarArgs := []string{"czf", varTarFile}
for _, pattern := range excludePatterns {
// We're handling the excluded patterns in bash, we need to single quote them to prevent expansion
args = append(args, "--exclude", fmt.Sprintf("'%s'", pattern))
tarArgs = append(tarArgs, "--exclude", fmt.Sprintf("'%s'", pattern))
}
args = append(args, "--selinux", sourceDir)
tarArgs = append(tarArgs, "--selinux", sourceDir)

// Run the tar command
err = runCMD("tar " + strings.Join(args, " "))
_, err = runInHostNamespace("tar", strings.Join(tarArgs, " "))
check(err)

log.Println("Backup of /var created successfully.")
Expand All @@ -181,37 +186,24 @@ func create() {
}

// Check if the backup file for /etc doesn't exist
if _, err := os.Stat(backupDir + "/etc.tgz"); os.IsNotExist(err) {
if _, err = os.Stat(backupDir + "/etc.tgz"); os.IsNotExist(err) {

// Execute 'ostree admin config-diff' command and backup /etc
ostreeAdminCMD := fmt.Sprintf(`nsenter --target 1 --cgroup --mount --ipc --pid -- ostree admin config-diff | awk '{print "/etc/" $2}' | xargs tar czf %s/etc.tgz --selinux`, backupDir)
err = runCMD(ostreeAdminCMD)
check(err)
_, ostreeAdminCMD := runInHostNamespace(
"ostree", []string{"admin", "config-diff", "|", "awk", `'{print "/etc/" $2}'`, "|", "xargs", "tar", "czf", backupDir + "/etc.tgz", "--selinux"}...)
check(ostreeAdminCMD)

log.Println("Backup of /etc created successfully.")
} else {
log.Println("Skipping etc backup as it already exists.")
}

// Check if the backup file for rpm-ostree doesn't exist
if _, err := os.Stat(backupDir + "/rpm-ostree.json"); os.IsNotExist(err) {

// Execute 'rpm-ostree status' command and backup its output
rpmOStreeCMD := fmt.Sprintf(`nsenter --target 1 --cgroup --mount --ipc --pid -- rpm-ostree status -v --json > %s/rpm-ostree.json`, backupDir)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So far we still need the rpm-ostree.json file for the restore process

I'm actually testing all "one image" process to see how it works, I'll keep you posted :)

Once we validate it, we probably should try the way Colin suggested and see how it goes :)

err = runCMD(rpmOStreeCMD)
check(err)

log.Println("Backup of rpm-ostree created successfully.")
} else {
log.Println("Skipping rpm-ostree backup as it already exists.")
}

// Check if the backup file for mco-currentconfig doesn't exist
if _, err = os.Stat(backupDir + "/mco-currentconfig.json"); os.IsNotExist(err) {

// Execute 'copy' command and backup mco-currentconfig
backupCurrentConfigCMD := fmt.Sprintf(`cp /etc/machine-config-daemon/currentconfig %s/mco-currentconfig.json`, backupDir)
err = runCMD(backupCurrentConfigCMD)
_, err = runInHostNamespace(
"cp", "/etc/machine-config-daemon/currentconfig", backupDir+"/mco-currentconfig.json")
check(err)

log.Println("Backup of mco-currentconfig created successfully.")
Expand All @@ -223,8 +215,8 @@ func create() {
if _, err = os.Stat(backupDir + "/ostree.commit"); os.IsNotExist(err) {

// Execute 'ostree commit' command
ostreeCommitCMD := fmt.Sprintf(`nsenter --target 1 --cgroup --mount --ipc --pid -- ostree commit --branch %s %s > %s/ostree.commit`, backupTag, backupDir, backupDir)
err = runCMD(ostreeCommitCMD)
_, err = runInHostNamespace(
"ostree", append([]string{"commit", "--branch", backupTag, backupDir}, ">", backupDir+"/ostree.commit")...)
check(err)

log.Debug("Commit backup created successfully.")
Expand All @@ -233,66 +225,67 @@ func create() {
}

//
// Encapsulating and pushing backup OCI image
// Building and pushing OCI image
//
log.Printf("Encapsulate and push backup OCI image to %s:%s.", containerRegistry, backupTag)
log.Printf("Build and push OCI image to %s:%s.", containerRegistry, backupTag)
log.Debug(rpmOstreeClient.RpmOstreeVersion()) // If verbose, also dump out current rpm-ostree version available

// Execute 'ostree container encapsulate' command for backup OCI image
ostreeEncapsulateBackupCMD := fmt.Sprintf(`nsenter --target 1 --cgroup --mount --ipc --pid sh -c 'REGISTRY_AUTH_FILE=%s ostree container encapsulate %s registry:%s:%s --repo /ostree/repo --label ostree.bootable=true'`, authFile, backupTag, containerRegistry, backupTag)
err = runCMD(ostreeEncapsulateBackupCMD)
// Get the current status of rpm-ostree daemon in the host
statusRpmOstree, err := rpmOstreeClient.QueryStatus()
check(err)

//
// Encapsulating and pushing base OCI image
//
log.Printf("Encapsulate and push base OCI image to %s:%s.", containerRegistry, baseTag)
// Get OSName for booted ostree deployment
bootedOSName := statusRpmOstree.Deployments[0].OSName

// Create base commit checksum file
ostreeBaseChecksumCMD := fmt.Sprintf(`nsenter --target 1 --cgroup --mount --ipc --pid -- rpm-ostree status -v --json | jq -r '.deployments[] | select(.booted == true).checksum' > /var/tmp/ostree.base.commit`)
err = runCMD(ostreeBaseChecksumCMD)
check(err)
// Get ID for booted ostree deployment
bootedID := statusRpmOstree.Deployments[0].ID

// Read base commit from file
baseCommit, err := readLineFromFile("/var/tmp/ostree.base.commit")
// Get SHA for booted ostree deployment
bootedDeployment := strings.Split(bootedID, "-")[1]

// Execute 'ostree container encapsulate' command for base OCI image
ostreeEncapsulateBaseCMD := fmt.Sprintf(`nsenter --target 1 --cgroup --mount --ipc --pid sh -c 'REGISTRY_AUTH_FILE=%s ostree container encapsulate %s registry:%s:%s --repo /ostree/repo --label ostree.bootable=true'`, authFile, baseCommit, containerRegistry, baseTag)
err = runCMD(ostreeEncapsulateBaseCMD)
check(err)
// Check if the backup file for .origin doesn't exist
originFileName := fmt.Sprintf("%s/ostree-%s.origin", backupDir, bootedDeployment)
if _, err = os.Stat(originFileName); os.IsNotExist(err) {

//
// Encapsulating and pushing parent OCI image
//

// Create parent checksum file
ostreeHasParentChecksumCMD := fmt.Sprintf(`nsenter --target 1 --cgroup --mount --ipc --pid -- rpm-ostree status -v --json | jq -r '.deployments[] | select(.booted == true) | has("base-checksum")' > /var/tmp/ostree.has.parent`)
err = runCMD(ostreeHasParentChecksumCMD)
check(err)

// Read hasParent commit from file
hasParent, err := readLineFromFile("/var/tmp/ostree.has.parent")

// Check if current ostree deployment has a parent commit
if hasParent == "true" {
log.Info("OCI image has a parent commit to be encapsulated.")

// Create parent commit checksum file
ostreeParentChecksumCMD := fmt.Sprintf(`nsenter --target 1 --cgroup --mount --ipc --pid -- rpm-ostree status -v --json | jq -r '.deployments[] | select(.booted == true)."base-checksum"' > /var/tmp/ostree.parent.commit`)
err = runCMD(ostreeParentChecksumCMD)
// Execute 'copy' command and backup .origin file
_, err = runInHostNamespace(
"cp", []string{"/ostree/deploy/" + bootedOSName + "/deploy/" + bootedDeployment + ".origin", originFileName}...)
check(err)

// Read parent commit from file
parentCommit, err := readLineFromFile("/var/tmp/ostree.parent.commit")
log.Println("Backup of .origin created successfully.")
} else {
log.Println("Skipping .origin backup as it already exists.")
}

// Execute 'ostree container encapsulate' command for parent OCI image
log.Printf("Encapsulate and push parent OCI image to %s:%s.", containerRegistry, parentTag)
ostreeEncapsulateParentCMD := fmt.Sprintf(`nsenter --target 1 --cgroup --mount --ipc --pid sh -c 'REGISTRY_AUTH_FILE=%s ostree container encapsulate %s registry:%s:%s --repo /ostree/repo --label ostree.bootable=true'`, authFile, parentCommit, containerRegistry, parentTag)
err = runCMD(ostreeEncapsulateParentCMD)
check(err)
// Create a temporary file for the Dockerfile content
tmpfile, err := os.CreateTemp("/var/tmp", "dockerfile-")
if err != nil {
log.Errorf("Error creating temporary file: %s", err)
}
defer os.Remove(tmpfile.Name()) // Clean up the temporary file

} else {
log.Info("Skipping encapsulate parent commit as it is not present.")
// Write the content to the temporary file
_, err = tmpfile.WriteString(containerFileContent)
if err != nil {
log.Errorf("Error writing to temporary file: %s", err)
}
tmpfile.Close() // Close the temporary file

// Build the single OCI image (note: We could include --squash-all option, as well)
leo8a marked this conversation as resolved.
Show resolved Hide resolved
_, err = runInHostNamespace(
"podman", []string{"build",
"-f", tmpfile.Name(),
"-t", containerRegistry + ":" + backupTag,
"--build-context", "ostreerepo=/sysroot/ostree/repo",
backupDir}...)
check(err)

// Push the created OCI image to user's repository
_, err = runInHostNamespace(
"podman", []string{"push",
"--authfile", authFile,
containerRegistry + ":" + backupTag}...)
check(err)

log.Printf("OCI image created successfully!")
}
110 changes: 110 additions & 0 deletions cmd/rpm-ostree-client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
Copyright 2023.

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.
*/

// This client lifts code from: https://github.com/coreos/rpmostree-client-go/blob/main/pkg/client/client.go

package cmd

import (
"encoding/json"
"fmt"

"gopkg.in/yaml.v3"
)

// Status summarizes the current worldview of the rpm-ostree daemon.
// The deployment list is the primary data.
type Status struct {
// Deployments is the list of bootable filesystem trees.
Deployments []Deployment
// Transaction is the active transaction, if any.
Transaction *[]string
}

// Deployment represents a bootable filesystem tree
type Deployment struct {
ID string `json:"id"`
OSName string `json:"osname"`
Serial int32 `json:"serial"`
BaseChecksum *string `json:"base-checksum"`
Checksum string `json:"checksum"`
Version string `json:"version"`
Timestamp uint64 `json:"timestamp"`
Booted bool `json:"booted"`
Staged bool `json:"staged"`
LiveReplaced string `json:"live-replaced,omitempty"`
Origin string `json:"origin"`
CustomOrigin []string `json:"custom-origin"`
ContainerImageReference string `json:"container-image-reference"`
RequestedPackages []string `json:"requested-packages"`
RequestedBaseRemovals []string `json:"requested-base-removals"`
Unlocked *string `json:"unlocked"`
}

// Client is a handle for interacting with a rpm-ostree based system.
type Client struct {
clientid string
}

// NewClient creates a new rpm-ostree client. The client identifier should be a short, unique and ideally machine-readable string.
// This could be as simple as `examplecorp-management-agent`.
// If you want to be more verbose, you could use a URL, e.g. `https://gitlab.com/examplecorp/management-agent`.
func NewClient(id string) Client {
return Client{
clientid: id,
}
}

func (client *Client) newCmd(args ...string) []byte {
rawOutput, _ := runInHostNamespace("rpm-ostree", args...)
return rawOutput
}

// VersionData represents the static information about rpm-ostree.
type VersionData struct {
Version string `yaml:"Version"`
Features []string `yaml:"Features"`
Git string `yaml:"Git"`
}

type rpmOstreeVersionData struct {
Root VersionData `yaml:"rpm-ostree"`
}

// RpmOstreeVersion returns the running rpm-ostree version number
func (client *Client) RpmOstreeVersion() (*VersionData, error) {
buf := client.newCmd("--version")

var q rpmOstreeVersionData

if err := yaml.Unmarshal(buf, &q); err != nil {
return nil, fmt.Errorf("failed to parse `rpm-ostree --version` output: %w", err)
}

return &q.Root, nil
}

// QueryStatus loads the current system state.
func (client *Client) QueryStatus() (*Status, error) {
var q Status
buf := client.newCmd("status", "--json")

if err := json.Unmarshal(buf, &q); err != nil {
return nil, fmt.Errorf("failed to parse `rpm-ostree status --json` output: %w", err)
}

return &q, nil
}
Loading