Skip to content

Commit

Permalink
✨ download BM images via ocr-registry.
Browse files Browse the repository at this point in the history
  • Loading branch information
guettli committed Dec 4, 2023
1 parent a12b364 commit 1415a1b
Show file tree
Hide file tree
Showing 5 changed files with 117 additions and 3 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ wait-and-get-secret:
$(KUBECTL) get secrets $(CLUSTER_NAME)-kubeconfig -o json | jq -r .data.value | base64 --decode > $(WORKER_CLUSTER_KUBECONFIG)
${TIMEOUT} 15m bash -c "while ! $(KUBECTL) --kubeconfig=$(WORKER_CLUSTER_KUBECONFIG) get nodes | grep control-plane; do sleep 1; done"

install-cilium-in-wl-cluster:
install-cilium-in-wl-cluster: $(HELM)
# Deploy cilium
$(HELM) repo add cilium https://helm.cilium.io/
$(HELM) repo update cilium
Expand Down
3 changes: 3 additions & 0 deletions api/v1beta1/hetznerbaremetalmachine_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,9 @@ func (bmMachine *HetznerBareMetalMachine) SetFailure(reason capierrors.MachineSt

// GetImageSuffix tests whether the suffix is known and outputs it if yes. Otherwise it returns an error.
func GetImageSuffix(url string) (string, error) {
if strings.HasPrefix(url, "oci://") {
return "tar.gz", nil
}
for _, suffix := range []ImageType{
ImageTypeTar,
ImageTypeTarGz,
Expand Down
85 changes: 85 additions & 0 deletions pkg/services/baremetal/client/ssh/download-from-oci.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
#!/bin/bash
set -euo pipefail

image="${1:-}"
outfile="${2:-}"

function usage {
echo "$0 image outfile."
echo " Download a machine image from a container registry"
echo " image: for example ghcr.io/foo/bar/node-images/staging/my-machine-image:v9"
echo " outfile: Created file. Usualy with file extensions '.tgz'"
echo " If the oci registry needs a token, then the script uses OCI_REGISTRY_AUTH_TOKEN (if set)"
echo " Example of OCI_REGISTRY_AUTH_TOKEN: github:ghp_SN51...."
echo
}
if [ -z "$outfile" ]; then
usage
exit 1
fi
OCI_REGISTRY_AUTH_TOKEN="${OCI_REGISTRY_AUTH_TOKEN:-}" # github:$GITHUB_TOKEN

# Extract registry
registry="${image%%/*}"

# Extract scope and tag
remainder="${image#*/}"
scope="${remainder%:*}"
tag="${remainder##*:}"

if [[ -z "$registry" || -z "$scope" || -z "$tag" ]]; then
echo "failed to parse registry, scope and tag from image"
echo "image=$image"
echo "registry=$registry"
echo "scope=$scope"
echo "tag=$tag"
exit 1
fi

function download_with_token {
echo "download with token (OCI_REGISTRY_AUTH_TOKEN set)"
if [[ "$OCI_REGISTRY_AUTH_TOKEN" != *:* ]]; then
echo "OCI_REGISTRY_AUTH_TOKEN needs to contain a ':' (user:token)"
exit 1
fi

token=$(curl -fsSL -u "$OCI_REGISTRY_AUTH_TOKEN" "https://${registry}/token?scope=repository:$scope:pull" | jq -r '.token')
if [ -z "$token" ]; then
echo "Failed to get token for container registry"
exit 1
fi

echo "Login to $registry was successful"

digest=$(curl -sSL -H "Authorization: Bearer $token" -H "Accept: application/vnd.oci.image.manifest.v1+json" \
"https://${registry}/v2/${scope}/manifests/${tag}" | jq -r '.layers[0].digest')

if [ -z "$digest" ]; then
echo "Failed to get digest from container registry"
exit 1
fi

echo "Start download of $image"
curl -fsSL -H "Authorization: Bearer $token" \
"https://${registry}/v2/${scope}/blobs/$digest" >"$outfile"
}

function download_without_token {
echo "download without token (OCI_REGISTRY_AUTH_TOKEN empty)"
digest=$(curl -sSL -H "Accept: application/vnd.oci.image.manifest.v1+json" \
"https://${registry}/v2/${scope}/manifests/${tag}" | jq -r '.layers[0].digest')

if [ -z "$digest" ]; then
echo "Failed to get digest from container registry"
exit 1
fi

echo "Start download of $image"
curl -fsSL "https://${registry}/v2/${scope}/blobs/$digest" >"$outfile"
}

if [ -z "$OCI_REGISTRY_AUTH_TOKEN" ]; then
download_without_token
else
download_with_token
fi
20 changes: 19 additions & 1 deletion pkg/services/baremetal/client/ssh/ssh_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ limitations under the License.
package sshclient

import (
_ "embed"

Check failure on line 21 in pkg/services/baremetal/client/ssh/ssh_client.go

View workflow job for this annotation

GitHub Actions / Lint Pull Request

File is not `gci`-ed with --skip-generated -s standard -s default -s prefix(github.com/syself/cluster-api-provider-hetzner) (gci)

"bufio"
"bytes"

Check failure on line 24 in pkg/services/baremetal/client/ssh/ssh_client.go

View workflow job for this annotation

GitHub Actions / Lint Pull Request

File is not `gci`-ed with --skip-generated -s standard -s default -s prefix(github.com/syself/cluster-api-provider-hetzner) (gci)
"encoding/base64"
Expand All @@ -35,6 +37,9 @@ const (
sshTimeOut time.Duration = 5 * time.Second
)

//go:embed download-from-oci.sh
var downloadFromOciShellScript string

var (
// ErrCommandExitedWithoutExitSignal means the ssh command exited unplanned.
ErrCommandExitedWithoutExitSignal = errors.New("wait: remote command exited without exit status or exit signal")
Expand Down Expand Up @@ -221,7 +226,20 @@ EOF`, data))

// DownloadImage implements the DownloadImage method of the SSHClient interface.
func (c *sshClient) DownloadImage(path, url string) Output {
return c.runSSH(fmt.Sprintf(`curl -sLo "%q" "%q"`, path, url))
fmt.Printf("########################### Start DownloadImage %s %s\n", path, url)
if !strings.HasPrefix(url, "oci://") {
return c.runSSH(fmt.Sprintf(`curl -sLo "%q" "%q"`, path, url))
}
out := c.runSSH(fmt.Sprintf(`cat << 'ENDOFSCRIPT' > /root/download-from-oci.sh
%s
ENDOFSCRIPT
chmod a+rx /root/download-from-oci.sh
OCI_REGISTRY_AUTH_TOKEN=%s /root/download-from-oci.sh %s %s`, downloadFromOciShellScript,
os.Getenv("OCI_REGISTRY_AUTH_TOKEN"),
strings.TrimPrefix(url, "oci://"), path))
fmt.Printf("########################### End DownloadImage err=%v\n %s\n %s\n", out.Err, out.StdOut, out.StdErr)
return out

Check failure on line 242 in pkg/services/baremetal/client/ssh/ssh_client.go

View workflow job for this annotation

GitHub Actions / Lint Pull Request

unnecessary trailing newline (whitespace)
}

// CreatePostInstallScript implements the CreatePostInstallScript method of the SSHClient interface.
Expand Down
10 changes: 9 additions & 1 deletion pkg/services/baremetal/host/host.go
Original file line number Diff line number Diff line change
Expand Up @@ -1011,7 +1011,15 @@ func (s *Service) createAutoSetupInput(sshClient sshclient.Client) (autoSetupInp
if needsDownload {
out := sshClient.DownloadImage(imagePath, image.URL)
if err := handleSSHError(out); err != nil {
return autoSetupInput{}, actionError{err: fmt.Errorf("failed to download image: %w", err)}
// TODO: this could fail like this, if the registry requires auth, but not token is given.
// TODO: This should be visible in the Conditions somehow.
// 15:40:37 ERROR "Reconciler error" controller/controller.go:324 {'HetznerBareMetalHost': ..
// 'error': 'failed to reconcile HetznerBareMetalHost default/test-bm-gpu: action "image-installing"
// failed: failed to download image: \ndownload without token (OCI_REGISTRY_AUTH_TOKEN)\n
// Start download of
// ghcr.io/syself/autopilot/node-images/staging/hetzner-apalla-1-27-workeramd64baremetal:v9-beta-1\n
// curl: (22) The requested URL returned error: 404\n failed to perform ssh command: Process exited with status 22'}
return autoSetupInput{}, actionError{err: fmt.Errorf("failed to download image: %s %s %w", out.StdOut, out.StdErr, err)}
}
}

Expand Down

0 comments on commit 1415a1b

Please sign in to comment.