Skip to content
Open
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
5 changes: 3 additions & 2 deletions cmd/crc/cmd/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,10 @@ func runStart(ctx context.Context) (*types.StartResult, error) {
isRunning, _ := client.IsRunning()

if !isRunning {
if err := checkDaemonStarted(); err != nil {
// TODO: Uncomment this when we need it only for admin-helper
/*if err := checkDaemonStarted(); err != nil {
return nil, err
}
}*/

if err := preflight.StartPreflightChecks(config); err != nil {
return nil, crcos.CodeExitError{
Expand Down
51 changes: 50 additions & 1 deletion pkg/crc/cache/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type Cache struct {
version string
ignoreNameMismatch bool
getVersion func(string) (string, error)
targetName string // Optional: if set, rename the executable to this name
}

type VersionMismatchError struct {
Expand Down Expand Up @@ -84,6 +85,49 @@ func NewAdminHelperCache() *Cache {
)
}

func NewGvproxyCache() *Cache {
url := constants.GetGvproxyURL()
version := version.GetGvproxyVersion()
cache := newCache(constants.GvproxyPath(),
url,
version,
func(executable string) (string, error) {
out, _, err := crcos.RunWithDefaultLocale(executable, "--version")
if err != nil {
return "", err
}
// gvproxy --version output format: "gvproxy version v0.8.7"
split := strings.Split(out, " ")
return strings.TrimSpace(split[len(split)-1]), nil
},
)
// Set target name to "gvproxy" (without platform suffix) since macadam expects this name
targetName := "gvproxy"
if strings.HasSuffix(cache.GetExecutableName(), ".exe") {
targetName = "gvproxy.exe"
}
cache.targetName = targetName
return cache
}

func NewMacadamCache() *Cache {
url := constants.GetMacadamURL()
version := version.GetMacadamVersion()
return newCache(constants.MacadamPath(),
url,
version,
func(executable string) (string, error) {
out, _, err := crcos.RunWithDefaultLocale(executable, "--version")
if err != nil {
return "", err
}
// macadam version output format: "macadam version v0.2.0"
split := strings.Split(out, " ")
return strings.TrimSpace(split[len(split)-1]), nil
},
)
}

func (c *Cache) IsCached() bool {
if _, err := os.Stat(c.GetExecutablePath()); os.IsNotExist(err) {
return false
Expand Down Expand Up @@ -136,7 +180,12 @@ func (c *Cache) cacheExecutable() error {

// Copy the requested asset into its final destination
for _, extractedFilePath := range extractedFiles {
finalExecutablePath := filepath.Join(constants.CrcBinDir, c.GetExecutableName())
// Use targetName if set, otherwise use the original executable name
finalName := c.GetExecutableName()
if c.targetName != "" {
finalName = c.targetName
}
finalExecutablePath := filepath.Join(constants.CrcBinDir, finalName)
// If the file exists then remove it (ignore error) first before copy because with `0500` permission
// it is not possible to overwrite the file.
os.Remove(finalExecutablePath)
Expand Down
106 changes: 106 additions & 0 deletions pkg/crc/cloudinit/cloudinit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package cloudinit

import (
"bytes"
"encoding/json"
"fmt"
"os"
"path/filepath"

"github.com/crc-org/crc/v2/pkg/crc/constants"
)

// UserDataOptions contains all the options needed to generate cloud-init user-data
type UserDataOptions struct {
PublicKey string
PullSecret string
KubeAdminPassword string
DeveloperPassword string
}

const userDataTemplate = `#cloud-config
runcmd:
- systemctl enable --now kubelet
write_files:
- path: /home/core/.ssh/authorized_keys
content: '%s'
owner: core
permissions: '0600'
- path: /opt/crc/id_rsa.pub
content: '%s'
owner: root:root
permissions: '0644'
- path: /etc/sysconfig/crc-env
content: |
CRC_SELF_SUFFICIENT=1
CRC_NETWORK_MODE_USER=1
owner: root:root
permissions: '0644'
- path: /opt/crc/pull-secret
content: |
%s
permissions: '0644'
- path: /opt/crc/pass_kubeadmin
content: '%s'
permissions: '0644'
- path: /opt/crc/pass_developer
content: '%s'
permissions: '0644'
- path: /opt/crc/ocp-custom-domain.service.done
permissions: '0644'
`

// compactJSON compacts a JSON string by removing whitespace and newlines
func compactJSON(jsonStr string) (string, error) {
var buf bytes.Buffer
if err := json.Compact(&buf, []byte(jsonStr)); err != nil {
return "", fmt.Errorf("failed to compact JSON: %w", err)
}
return buf.String(), nil
}

// GenerateUserData generates a cloud-init user-data file and returns the path
func GenerateUserData(machineName string, opts UserDataOptions) (string, error) {
// Create the machine directory if it doesn't exist
machineDir := filepath.Join(constants.MachineInstanceDir, machineName)
if err := os.MkdirAll(machineDir, 0750); err != nil {
return "", fmt.Errorf("failed to create machine directory: %w", err)
}

// Compact the pull secret JSON
compactPullSecret, err := compactJSON(opts.PullSecret)
if err != nil {
return "", fmt.Errorf("failed to compact pull secret: %w", err)
}

// Generate the cloud-init user-data content
userData := fmt.Sprintf(userDataTemplate,
opts.PublicKey, // /home/core/.ssh/authorized_keys
opts.PublicKey, // /opt/crc/id_rsa.pub
compactPullSecret, // /opt/crc/pull-secret (compacted)
opts.KubeAdminPassword, // /opt/crc/pass_kubeadmin
opts.DeveloperPassword, // /opt/crc/pass_developer
)

// Write the user-data file
userDataPath := filepath.Join(machineDir, "user-data")
if err := os.WriteFile(userDataPath, []byte(userData), 0600); err != nil {
return "", fmt.Errorf("failed to write user-data file: %w", err)
}

return userDataPath, nil
}

// RemoveUserData removes the cloud-init user-data file for a machine
func RemoveUserData(machineName string) error {
userDataPath := filepath.Join(constants.MachineInstanceDir, machineName, "user-data")
if err := os.Remove(userDataPath); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to remove user-data file: %w", err)
}
return nil
}

// GetUserDataPath returns the path to the user-data file for a machine
func GetUserDataPath(machineName string) string {
return filepath.Join(constants.MachineInstanceDir, machineName, "user-data")
}
199 changes: 199 additions & 0 deletions pkg/crc/cloudinit/cloudinit_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
package cloudinit

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

"github.com/crc-org/crc/v2/pkg/crc/constants"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestGenerateUserData(t *testing.T) {
// Create a temporary directory for testing
tempDir := t.TempDir()
oldMachineInstanceDir := constants.MachineInstanceDir
constants.MachineInstanceDir = tempDir
defer func() {
constants.MachineInstanceDir = oldMachineInstanceDir
}()

opts := UserDataOptions{
PublicKey: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMockPublicKey [email protected]",
PullSecret: `{"auths":{"registry.redhat.io":{"auth":"mockauth"}}}`,
KubeAdminPassword: "test-kubeadmin-pass",
DeveloperPassword: "test-developer-pass",
}

userDataPath, err := GenerateUserData("test-machine", opts)
require.NoError(t, err)

// Verify the file was created
assert.FileExists(t, userDataPath)

// Read and verify the content
content, err := os.ReadFile(userDataPath)
require.NoError(t, err)

contentStr := string(content)

// Verify cloud-config header
assert.Contains(t, contentStr, "#cloud-config")

// Verify runcmd section
assert.Contains(t, contentStr, "runcmd:")
assert.Contains(t, contentStr, "systemctl enable --now kubelet")

// Verify SSH key is present
assert.Contains(t, contentStr, opts.PublicKey)
assert.Contains(t, contentStr, "/home/core/.ssh/authorized_keys")

// Verify pull secret
assert.Contains(t, contentStr, opts.PullSecret)
assert.Contains(t, contentStr, "/opt/crc/pull-secret")

// Verify passwords
assert.Contains(t, contentStr, opts.KubeAdminPassword)
assert.Contains(t, contentStr, opts.DeveloperPassword)

// Verify CRC environment variables
assert.Contains(t, contentStr, "CRC_SELF_SUFFICIENT=1")
assert.Contains(t, contentStr, "CRC_NETWORK_MODE_USER=1")

// Verify file paths
assert.Contains(t, contentStr, "/etc/sysconfig/crc-env")
assert.Contains(t, contentStr, "/opt/crc/pass_kubeadmin")
assert.Contains(t, contentStr, "/opt/crc/pass_developer")
}

func TestGetUserDataPath(t *testing.T) {
tempDir := t.TempDir()
oldMachineInstanceDir := constants.MachineInstanceDir
constants.MachineInstanceDir = tempDir
defer func() {
constants.MachineInstanceDir = oldMachineInstanceDir
}()

path := GetUserDataPath("test-machine")
expectedPath := filepath.Join(tempDir, "test-machine", "user-data")
assert.Equal(t, expectedPath, path)
}

func TestRemoveUserData(t *testing.T) {
tempDir := t.TempDir()
oldMachineInstanceDir := constants.MachineInstanceDir
constants.MachineInstanceDir = tempDir
defer func() {
constants.MachineInstanceDir = oldMachineInstanceDir
}()

// Create a user-data file
opts := UserDataOptions{
PublicKey: "test-key",
PullSecret: `{"auths":{"test.io":{"auth":"testauth"}}}`,
KubeAdminPassword: "test-kubeadmin",
DeveloperPassword: "test-developer",
}

userDataPath, err := GenerateUserData("test-machine", opts)
require.NoError(t, err)
assert.FileExists(t, userDataPath)

// Remove the file
err = RemoveUserData("test-machine")
require.NoError(t, err)
assert.NoFileExists(t, userDataPath)

// Removing non-existent file should not error
err = RemoveUserData("non-existent-machine")
assert.NoError(t, err)
}

func TestGenerateUserDataMultilineContent(t *testing.T) {
tempDir := t.TempDir()
oldMachineInstanceDir := constants.MachineInstanceDir
constants.MachineInstanceDir = tempDir
defer func() {
constants.MachineInstanceDir = oldMachineInstanceDir
}()

opts := UserDataOptions{
PublicKey: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMockPublicKey [email protected]",
PullSecret: `{
"auths": {
"registry.redhat.io": {
"auth": "mockauth"
}
}
}`,
KubeAdminPassword: "test-kubeadmin-pass",
DeveloperPassword: "test-developer-pass",
}

userDataPath, err := GenerateUserData("test-machine", opts)
require.NoError(t, err)

content, err := os.ReadFile(userDataPath)
require.NoError(t, err)

// Verify pull secret is compacted (no newlines or extra spaces)
contentStr := string(content)
assert.Contains(t, contentStr, "registry.redhat.io")
// Should be compacted, not the original multiline format
assert.Contains(t, contentStr, `{"auths":{"registry.redhat.io":{"auth":"mockauth"}}}`)
// Should NOT contain the multiline version with spaces
assert.NotContains(t, contentStr, " \"auths\"")
}

func TestCompactJSON(t *testing.T) {
tests := []struct {
name string
input string
expected string
wantErr bool
}{
{
name: "multiline JSON",
input: `{
"auths": {
"registry.redhat.io": {
"auth": "mockauth"
}
}
}`,
expected: `{"auths":{"registry.redhat.io":{"auth":"mockauth"}}}`,
wantErr: false,
},
{
name: "already compact JSON",
input: `{"auths":{"registry.redhat.io":{"auth":"mockauth"}}}`,
expected: `{"auths":{"registry.redhat.io":{"auth":"mockauth"}}}`,
wantErr: false,
},
{
name: "invalid JSON",
input: `{invalid json}`,
expected: "",
wantErr: true,
},
{
name: "empty object",
input: `{}`,
expected: `{}`,
wantErr: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := compactJSON(tt.input)
if tt.wantErr {
assert.Error(t, err)
} else {
require.NoError(t, err)
assert.Equal(t, tt.expected, result)
}
})
}
}
Loading
Loading