Skip to content
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
15 changes: 15 additions & 0 deletions .github/workflows/promote.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
name: promote

on:
push:
branches: [prod-staging]
workflow_dispatch:

permissions:
contents: read

jobs:
promote:
uses: unikraft-cloud/x/.github/workflows/promote.yaml@prod-staging
secrets:
gh_pat: ${{ secrets.GH_PAT }}
1 change: 1 addition & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ jobs:
UNIKRAFT_TOKEN: ${{ secrets.UNIKRAFT_TOKEN }}
UNIKRAFT_ENDPOINT: ${{ secrets[format('UNIKRAFT_ENDPOINT_{0}', matrix.target)] }}
UNIKRAFT_X_TEST_SERVER: ${{ env[format('UNIKRAFT_SERVER_{0}', matrix.target)] }}
UNIKRAFT_X_TEST_BUILD_URL: ${{ matrix.target == 'STABLE' && secrets.UNIKRAFT_BUILD_URL_STABLE || '' }}

# The hosted Windows runners reach only the public prod endpoint. The runners
# take turns, so that both do not use the same organization at once.
Expand Down
4 changes: 4 additions & 0 deletions cmd/unikraft/help_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,10 @@ func buildHelpTests(t *testing.T, unikraftPath string) {
r := integ.NewTestEnv(t, unikraftPath)
integ.Gild(t, cli(r),
[]string{"unikraft", "build", "--help"},
[]string{"unikraft", "builder", "--help"},
[]string{"unikraft", "builder", "create", "--help"},
[]string{"unikraft", "builder", "du", "--help"},
[]string{"unikraft", "builder", "prune", "--help"},
)
}

Expand Down
48 changes: 48 additions & 0 deletions cmd/unikraft/integration/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
package integration

import (
"encoding/pem"
"fmt"
"os"
"path/filepath"
"testing"

"github.com/containerd/continuity/fs/fstest"
Expand Down Expand Up @@ -318,4 +321,49 @@ cmd: ["sh", "-c", "cat /etc/unikraft-e2e && echo KRAFTFILE_CMD_USED"]

r.Run(t, []string{"unikraft", "instance", "delete", "test-" + instName})
})

t.Run("remote", func(t *testing.T) {
buildURL := os.Getenv("UNIKRAFT_X_TEST_BUILD_URL")
if buildURL == "" {
t.Skip("remote build test requires UNIKRAFT_X_TEST_BUILD_URL")
}
r := runner(t, true, []string{staging, stable})

image := r.Profile.Organization + "/remote-e2e:" + uniq()
instName := uniq()

// The build service's certificate may be self-signed.
certFile := filepath.Join(t.TempDir(), "build-service.pem")
cert := integ.HTTPGetTLSCerts(t, buildURL, "")[0]
require.NoError(t, os.WriteFile(certFile, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}), 0o644))

dir := t.TempDir()
require.NoError(t, fstest.Apply(
fstest.CreateFile("Dockerfile", []byte(`
FROM busybox:latest
RUN --mount=type=secret,id=greeting cp /run/secrets/greeting /greeting
`), 0o644),
fstest.CreateFile("Kraftfile", []byte(`
spec: v0.7
runtime: base-compat:latest
rootfs:
format: erofs
source: ./Dockerfile
cmd: ["sh", "-c", "cat /greeting"]
`), 0o644),
fstest.CreateFile("greeting", []byte("REMOTE_BUILD_OK\n"), 0o644),
).Apply(dir))

// The service reads the project and the secret through the session.
r.Run(t, []string{"unikraft", "build", ".", "--remote", buildURL, "--secret", "id=greeting,src=" + filepath.Join(dir, "greeting"), "--output", image},
integ.WithWorkDir(dir), integ.WithEnv("SSL_CERT_FILE="+certFile))

r.Run(t, []string{"unikraft", "run", "--name", "test-" + instName, "--metro", r.MetroName, "--output", "quiet", "--image", image})
r.Run(t, []string{"unikraft", "--timeout", "10s", "instance", "wait", "--until", "state==stopped", "test-" + instName})

out := r.Run(t, []string{"unikraft", "instance", "logs", "test-" + instName})
assert.Regexp(t, `REMOTE_BUILD_OK`, out)

r.Run(t, []string{"unikraft", "instance", "delete", "test-" + instName})
})
}
42 changes: 42 additions & 0 deletions cmd/unikraft/integration/builder_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// SPDX-License-Identifier: BSD-3-Clause
// Copyright (c) 2026, Unikraft GmbH and The Unikraft CLI Authors.
// Licensed under the BSD-3-Clause License (the "License").
// You may not use this file except in compliance with the License.

package integration

import (
"encoding/pem"
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

integ "unikraft.com/cli/internal/integration"
)

func TestBuilder(t *testing.T) {
buildURL := os.Getenv("UNIKRAFT_X_TEST_BUILD_URL")
if buildURL == "" {
t.Skip("builder test requires UNIKRAFT_X_TEST_BUILD_URL")
}
r := runner(t, true, []string{staging, stable})

// The build service's certificate may be self-signed.
certFile := filepath.Join(t.TempDir(), "build-service.pem")
cert := integ.HTTPGetTLSCerts(t, buildURL, "")[0]
require.NoError(t, os.WriteFile(certFile, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}), 0o644))
env := integ.WithEnv("SSL_CERT_FILE="+certFile, "UNIKRAFT_BUILD_URL="+buildURL)

// The account's remote builds share the builder, so it is not deleted.
out := r.Run(t, []string{"unikraft", "builder", "create"}, env)
assert.Regexp(t, `builder ready`, out)

out = r.Run(t, []string{"unikraft", "builder", "du"}, env)
assert.Regexp(t, `(?m)^Reclaimable: +\S+\nTotal: +\S+$`, out)

out = r.Run(t, []string{"unikraft", "builder", "prune"}, env)
assert.Regexp(t, `pruned the builder's cache freed=\S+`, out)
}
85 changes: 66 additions & 19 deletions cmd/unikraft/integration/instance_http_oci_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,40 @@ import (
)

// httpOCIServer serves a volume mounted at /data as http+oci layouts: the node
// only fetches a layout if it is served with the layout media type.
// only fetches a layout if it is served with the layout media type. A tag is a
// file holding the digest it redirects to.
var httpOCIServer = &integ.SharedImage{
Name: "http-oci-server-e2e",
Files: map[string]string{
"main.go": `package main

import "net/http"
import (
"io"
"net/http"
"path"
"strings"
)

func main() {
files := http.FileServer(http.Dir("/data"))
data := http.Dir("/data")
files := http.FileServer(data)
http.ListenAndServe(":8080", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
dir, ref := path.Split(r.URL.Path)
if !strings.HasPrefix(ref, "@") {
f, err := data.Open(r.URL.Path)
if err != nil {
http.NotFound(w, r)
return
}
defer f.Close()
dgst, err := io.ReadAll(f)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
http.Redirect(w, r, dir+strings.TrimSpace(string(dgst)), http.StatusFound)
return
}
w.Header().Set("Content-Type", "application/vnd.oci.layout.v1+tar")
files.ServeHTTP(w, r)
}))
Expand All @@ -60,20 +83,20 @@ cmd: ["/server"]
},
}

func TestInstancesHTTPOCI(t *testing.T) {
func TestHTTPOCI(t *testing.T) {
r := runner(t, true, []string{staging, stable})
payload := integ.Busybox.Build(t, r)
server := httpOCIServer.Build(t, r)
volName := "test-" + uniq()
serverName := "test-" + uniq()
instName := "test-" + uniq()

layout := filepath.Join(t.TempDir(), "layout")
r.Run(t, []string{"unikraft", "image", "copy", payload, "oci-layout://" + layout})

repository := r.Profile.Organization + "/http-oci-e2e"
content := t.TempDir()
dgst := tarLayout(t, layout, filepath.Join(content, repository))
require.NoError(t, os.WriteFile(filepath.Join(content, repository, "latest"), []byte("@"+dgst), 0o644))

r.Run(t, []string{
"unikraft", "volume", "create", "--output", "quiet",
Expand All @@ -86,27 +109,51 @@ func TestInstancesHTTPOCI(t *testing.T) {
"unikraft", "run", "--name", serverName, "--metro", r.MetroName, "--output", "quiet",
"--image", server, "-p", "80:8080/http", "-v", volName + ":/data",
})
t.Cleanup(func() {
r.Run(t, []string{"unikraft", "instance", "delete", serverName}, integ.WithoutCancel())
})
r.Run(t, []string{"unikraft", "--timeout", "30s", "instance", "wait", "--until", "state==running", serverName})
fqdn := strings.TrimSpace(r.Run(t, []string{
"unikraft", "instance", "inspect", serverName,
"--output", "template={{ range .service.domains }}{{ .fqdn }}{{ end }}",
}))
require.NotEmpty(t, fqdn)

image := "http+oci://" + fqdn + "/" + repository + "/@" + dgst
r.Run(t, []string{
"unikraft", "run", "--name", instName, "--metro", r.MetroName, "--output", "quiet",
"--image", image, "--args", "echo UNIKRAFT_HTTP_OCI_OK",
})
r.Run(t, []string{"unikraft", "--timeout", "60s", "instance", "wait", "--until", "state==stopped", instName})

out := r.Run(t, []string{"unikraft", "instance", "inspect", instName})
assert.Contains(t, out, image)
out = r.Run(t, []string{"unikraft", "instance", "logs", instName})
assert.Contains(t, out, "UNIKRAFT_HTTP_OCI_OK")

r.Run(t, []string{"unikraft", "instance", "delete", instName})
r.Run(t, []string{"unikraft", "instance", "delete", serverName})
for name, image := range map[string]string{
"tag": "http+oci://" + fqdn + "/" + repository + "/latest",
"digest": "http+oci://" + fqdn + "/" + repository + "/@" + dgst,
} {
t.Run(name, func(t *testing.T) {
t.Run("inspect", func(t *testing.T) {
out := r.Run(t, []string{"unikraft", "image", "inspect", image})
assert.Contains(t, out, image)
assert.Regexp(t, `platform:\s+kraftcloud/x86_64`, out)
})

t.Run("copy", func(t *testing.T) {
archive := filepath.Join(t.TempDir(), "image.oci.tar")
r.Run(t, []string{"unikraft", "image", "copy", image, archive})
out := r.Run(t, []string{"unikraft", "image", "inspect", archive})
assert.Regexp(t, `platform:\s+kraftcloud/x86_64`, out)
})

t.Run("run", func(t *testing.T) {
instName := "test-" + uniq()
r.Run(t, []string{
"unikraft", "run", "--name", instName, "--metro", r.MetroName, "--output", "quiet",
"--image", image, "--args", "echo UNIKRAFT_HTTP_OCI_OK",
})
r.Run(t, []string{"unikraft", "--timeout", "60s", "instance", "wait", "--until", "state==stopped", instName})

out := r.Run(t, []string{"unikraft", "instance", "inspect", instName})
assert.Contains(t, out, fqdn+"/"+repository)
out = r.Run(t, []string{"unikraft", "instance", "logs", instName})
assert.Contains(t, out, "UNIKRAFT_HTTP_OCI_OK")

r.Run(t, []string{"unikraft", "instance", "delete", instName})
})
})
}
}

// tarLayout packs the layout at src into dir, named by its digest as http+oci
Expand Down
2 changes: 2 additions & 0 deletions cmd/unikraft/integration/sandbox_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ func TestSandbox(t *testing.T) {
// whatever the command printed.
r.Run(t, []string{"unikraft", "instance", "exec", instName, "--", "sh", "-c", "exit 3"},
integ.ExpectExitCode(3))
r.Run(t, []string{"unikraft", "instance", "exec", instName, "--", "sh", "-c", "kill -KILL $$"},
integ.ExpectExitCode(137))

// The command is a command line by the time it reaches the plugin, so
// arguments the shell would otherwise split or expand are quoted first.
Expand Down
Loading
Loading