Skip to content

Commit

Permalink
blueprints: Add cloudapi support for depsolving local blueprint files
Browse files Browse the repository at this point in the history
If the cloudapi is available it will upload the local blueprint file and
depsolve it. It defaults to using the host's distribution and
architecture but these can be overridden with the `--distro` and
`--arch` cmdline arguments.

Resolves: RHEL-60126
  • Loading branch information
bcl committed Mar 10, 2025
1 parent 45c4307 commit e24ddc4
Show file tree
Hide file tree
Showing 2 changed files with 153 additions and 21 deletions.
104 changes: 83 additions & 21 deletions cmd/composer-cli/blueprints/depsolve.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,49 +6,111 @@ package blueprints

import (
"fmt"
"io"
"os"

"github.com/BurntSushi/toml"
"github.com/spf13/cobra"

"github.com/osbuild/weldr-client/v2/cmd/composer-cli/root"
"github.com/osbuild/weldr-client/v2/internal/common"
"github.com/osbuild/weldr-client/v2/weldr"
)

var (
depsolveCmd = &cobra.Command{
Use: "depsolve BLUEPRINT,...",
Short: "Depsolve the blueprints and output the package lists",
Example: " composer-cli blueprints depsolve tmux-image",
RunE: depsolve,
Args: cobra.MinimumNArgs(1),
Use: "depsolve BLUEPRINT,...",
Short: "Depsolve the blueprints and output the package lists",
Example: ` composer-cli blueprints depsolve tmux-image
composer-cli blueprints depsolve ./tmux-image.toml
composer-cli blueprints depsolve --distro fedora-36 ./tmux-image.toml
composer-cli blueprints depsolve --distro fedora-36 --arch aarch64 ./tmux-image.toml`,
RunE: depsolve,
Args: cobra.MinimumNArgs(1),
}
distro string
arch string
)

func init() {
depsolveCmd.Flags().StringVarP(&distro, "distro", "", "", "Distribution")
depsolveCmd.Flags().StringVarP(&arch, "arch", "", "", "Architecture")
blueprintsCmd.AddCommand(depsolveCmd)
}

func depsolve(cmd *cobra.Command, args []string) (rcErr error) {
names := root.GetCommaArgs(args)
response, errors, err := root.Client.DepsolveBlueprints(names)
if err != nil {
return root.ExecutionError(cmd, "Depsolve Error: %s", err)
}
if len(errors) > 0 {
rcErr = root.ExecutionErrors(cmd, errors)
}
// Is the blueprint a local file? If so, try to use the cloud API for the depsolve
f, err := os.Open(args[0])
if err == nil {
defer f.Close()

bps, err := weldr.ParseDepsolveResponse(response)
if err != nil {
return root.ExecutionError(cmd, "Depsolve Error: %s", err)
}
if !root.Cloud.Exists() {
return root.ExecutionError(cmd, "Using a local blueprint requires server support. Check to make sure that the cloudapi socket is enabled.")
}

if len(distro) == 0 {
distro, err = common.GetHostDistroName()
if err != nil {
return root.ExecutionError(cmd, "Error determining host distribution: %s", err)
}
}

if len(arch) == 0 {
arch = common.HostArch()
}

data, err := io.ReadAll(f)
if err != nil {
return root.ExecutionError(cmd, "reading %s - %s", args[0], err)
}
var blueprint interface{}
err = toml.Unmarshal([]byte(data), &blueprint)
if err != nil {
return root.ExecutionError(cmd, "reading %s - %s", args[0], err)
}

deps, err := root.Cloud.DepsolveBlueprint(blueprint, distro, arch)
if err != nil {
return root.ExecutionError(cmd, "Depsolve Error: %s", err)
}

// Get the blueprint name and version
var bpNameVersion struct {
Name string
Version string
}
err = toml.Unmarshal([]byte(data), &bpNameVersion)
if err != nil {
return root.ExecutionError(cmd, "reading %s - %s", args[0], err)
}

for _, bp := range bps {
fmt.Printf("blueprint: %s v%s\n", bp.Blueprint.Name, bp.Blueprint.Version)
for _, d := range bp.Dependencies {
fmt.Printf("blueprint: %s v%s\n", bpNameVersion.Name, bpNameVersion.Version)
for _, d := range deps {
fmt.Printf(" %s\n", d)
}
}
} else {

names := root.GetCommaArgs(args)
response, errors, err := root.Client.DepsolveBlueprints(names)
if err != nil {
return root.ExecutionError(cmd, "Depsolve Error: %s", err)
}
if len(errors) > 0 {
rcErr = root.ExecutionErrors(cmd, errors)
}

bps, err := weldr.ParseDepsolveResponse(response)
if err != nil {
return root.ExecutionError(cmd, "Depsolve Error: %s", err)
}

for _, bp := range bps {
fmt.Printf("blueprint: %s v%s\n", bp.Blueprint.Name, bp.Blueprint.Version)
for _, d := range bp.Dependencies {
fmt.Printf(" %s\n", d)
}
}
}
// If there were any errors, even if other blueprints succeeded, it returns an error
return rcErr
}
70 changes: 70 additions & 0 deletions cmd/composer-cli/blueprints/depsolve_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"bytes"
"io"
"net/http"
"os"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -274,3 +275,72 @@ func TestCmdBlueprintsBadDepsolveJSON(t *testing.T) {
assert.Equal(t, "GET", mc.Req.Method)
assert.Equal(t, "/api/v1/blueprints/depsolve/cli-test-bp-1", mc.Req.URL.Path)
}

func TestCmdBlueprintsDepsolveLocalBP(t *testing.T) {
// Test the "blueprint depsolve" command with a local blueprint file
mcc := root.SetupCloudCmdTest(func(request *http.Request) (*http.Response, error) {
json := `{
"packages": [
{
"arch": "x86_64",
"checksum": "sha256:4e8d09770255a4945b86a8842282bda5c9e08717d67c1e0115d8804653535c86",
"name": "tmux",
"release": "2.fc41",
"type": "rpm",
"version": "3.5a"
},
{
"arch": "x86_64",
"checksum": "sha256:05486c33ff403f74fd3242e878900decf743ecafe809f5a65b95f16c9cd83165",
"epoch": "2",
"name": "vim-enhanced",
"release": "1.fc41",
"type": "rpm",
"version": "9.1.1081"
}
]
}`

return &http.Response{
StatusCode: 200,
Body: io.NopCloser(bytes.NewReader([]byte(json))),
}, nil
})

// Need a temporary test file
tmpBP, err := os.CreateTemp("", "test-bp-p*.toml")
require.Nil(t, err)
defer os.Remove(tmpBP.Name())

_, err = tmpBP.Write([]byte(`name = "test bp"
version = "1.1.0"
[[packages]]
name = "tmux"
version = "3.5a"
`))
require.Nil(t, err)

// Start a depsolve
cmd, out, err := root.ExecuteTest("blueprints", "depsolve", tmpBP.Name())
defer out.Close()
require.Nil(t, err)
require.NotNil(t, cmd)
assert.Equal(t, cmd, depsolveCmd)
require.NotNil(t, out.Stdout)
require.NotNil(t, out.Stderr)
stdout, err := io.ReadAll(out.Stdout)
assert.Nil(t, err)
assert.Contains(t, string(stdout), "blueprint: test bp v1.1.0")
assert.Contains(t, string(stdout), "vim-enhanced-2:9.1.1081-1.fc41.x86_64")
assert.Contains(t, string(stdout), "tmux-3.5a-2.fc41.x86_64")
stderr, err := io.ReadAll(out.Stderr)
assert.Nil(t, err)
assert.Equal(t, []byte(""), stderr)
assert.Equal(t, "POST", mcc.Req.Method)
sentBody, err := io.ReadAll(mcc.Req.Body)
mcc.Req.Body.Close()
require.Nil(t, err)
assert.Contains(t, string(sentBody), `"blueprint":{"name":"test bp","packages":[{"name":"tmux","version":"3.5a"}],"version":"1.1.0"}`)
assert.Equal(t, "application/json", mcc.Req.Header.Get("Content-Type"))
assert.Equal(t, "/api/image-builder-composer/v2/depsolve/blueprint", mcc.Req.URL.Path)
}

0 comments on commit e24ddc4

Please sign in to comment.