Skip to content
Draft
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
31 changes: 31 additions & 0 deletions .github/actions/slack-notify/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Copyright (c) Codesphere Inc.
# SPDX-License-Identifier: Apache-2.0

# Posts a failure notification to the #team-oms-sdk Slack channel.
name: Slack Notification on Failure
description: Notify #team-oms-sdk on job failure

inputs:
job-name:
description: Display name of the failing job (e.g. "Build", "Test", "Install-Config")
required: true
webhook:
description: Slack webhook URL for the notification
required: true

runs:
using: composite
steps:
- name: Post failure to Slack
shell: bash
env:
SLACK_WEBHOOK: ${{ inputs.webhook }}
run: |
curl -X POST \
-H 'Content-type: application/json' \
--data "{
\"channel\": \"#team-oms-sdk\",
\"icon_emoji\": \":siren:\",
\"text\": \"🚨 ${{ inputs.job-name }} Job Failure 🚨\nRepository: ${{ github.repository }}\nBranch/Ref: \`${{ github.ref }}\`\nRun url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\"
}" \
"${SLACK_WEBHOOK}"
73 changes: 40 additions & 33 deletions .github/workflows/cli-build_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,19 @@ on:

jobs:

build:
build-test:
name: ${{ matrix.label }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- task: build
label: Build
- task: test
label: Test
- task: install-config
label: Install-Config
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6

Expand All @@ -24,42 +35,38 @@ jobs:
with:
go-version-file: 'go.mod'

- name: Build
- name: Build CLI
if: ${{ matrix.task == 'build' || matrix.task == 'install-config' }}
run: make build-cli

- name: Slack Notification on Failure
if: ${{ failure() && github.ref == 'refs/heads/main' }}
run: |
curl -X POST \
-H 'Content-type: application/json' \
--data '{
"channel": "#team-oms-sdk",
"icon_emoji": ":siren:",
"text": "🚨 Build Job Failure 🚨\nRepository: ${{ github.repository }}\nBranch/Ref: `${{ github.ref }}`\nRun url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
}' \
${{ secrets.PACKAGE_JOB_SLACK_WEBHOOK }}

test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6

- name: Set up Go
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6
with:
go-version-file: 'go.mod'

- name: Test
if: ${{ matrix.task == 'test' }}
run: make test

- name: Generate and validate install configs
if: ${{ matrix.task == 'install-config' }}
run: |
set -euo pipefail
for profile in dev minimal production; do
echo "==> Generating install config for profile: ${profile}"
./oms init install-config \
--profile "${profile}" \
--interactive=false \
-c "config-${profile}.yaml" \
--vault "prod-${profile}.vault.yaml"

echo "==> Validating generated install config for profile: ${profile}"
# Validate the config only; the freshly generated vault is plaintext and
# --validate requires a SOPS-encrypted vault to validate vault contents.
./oms init install-config \
--validate \
-c "config-${profile}.yaml" \
--vault ""
done

- name: Slack Notification on Failure
if: ${{ failure() && github.ref == 'refs/heads/main' }}
run: |
curl -X POST \
-H 'Content-type: application/json' \
--data '{
"channel": "#team-oms-sdk",
"icon_emoji": ":siren:",
"text": "🚨 Build Job Failure 🚨\nRepository: ${{ github.repository }}\nBranch/Ref: `${{ github.ref }}`\nRun url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
}' \
${{ secrets.PACKAGE_JOB_SLACK_WEBHOOK }}
uses: ./.github/actions/slack-notify
with:
job-name: ${{ matrix.label }}
webhook: ${{ secrets.PACKAGE_JOB_SLACK_WEBHOOK }}
15 changes: 10 additions & 5 deletions cli/cmd/init_install_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,8 @@ func AddInitInstallConfigCmd(init *cobra.Command, opts *util.GlobalOptions) {
// K8s
c.cmd.Flags().BoolVar(&c.Opts.KubernetesManagedByCodesphere, "k8s-managed", true, "Use Codesphere-managed Kubernetes")
c.cmd.Flags().StringSliceVar(&c.Opts.KubernetesControlPlanes, "k8s-control-plane", []string{}, "K8s control plane IPs (comma-separated)")
c.cmd.Flags().StringVar(&c.Opts.KubernetesPodCIDR, "k8s-pod-cidr", "", "Pod CIDR (required when --k8s-managed=false)")
c.cmd.Flags().StringVar(&c.Opts.KubernetesServiceCIDR, "k8s-service-cidr", "", "Service CIDR (required when --k8s-managed=false)")

// Ceph
c.cmd.Flags().StringVar(&c.Opts.CephCsiKubeletDir, "ceph-csi-kubelet-dir", "", "Directory of kubelet for ceph csi. Required for some cloud providers")
Expand Down Expand Up @@ -403,6 +405,9 @@ func (c *InitInstallConfigCmd) updateConfigFromOpts(config *files.RootConfig, va
}

// Kubernetes settings
if c.cmd != nil && c.cmd.Flags().Changed("k8s-managed") {
config.Kubernetes.ManagedByCodesphere = c.Opts.KubernetesManagedByCodesphere
}
if c.Opts.KubernetesAPIServerHost != "" {
config.Kubernetes.APIServerHost = c.Opts.KubernetesAPIServerHost
}
Expand Down Expand Up @@ -449,18 +454,18 @@ func (c *InitInstallConfigCmd) updateConfigFromOpts(config *files.RootConfig, va

// MetalLB settings
if c.Opts.MetalLBEnabled {
if config.MetalLB == nil {
config.MetalLB = &files.MetalLBConfig{
if config.Cluster.MetalLB == nil {
config.Cluster.MetalLB = &files.MetalLBConfig{
Enabled: c.Opts.MetalLBEnabled,
Pools: []files.MetalLBPoolDef{},
}
} else {
config.MetalLB.Enabled = c.Opts.MetalLBEnabled
config.MetalLB.Pools = []files.MetalLBPoolDef{}
config.Cluster.MetalLB.Enabled = c.Opts.MetalLBEnabled
config.Cluster.MetalLB.Pools = []files.MetalLBPoolDef{}
}

for _, pool := range c.Opts.MetalLBPools {
config.MetalLB.Pools = append(config.MetalLB.Pools, files.MetalLBPoolDef(pool))
config.Cluster.MetalLB.Pools = append(config.Cluster.MetalLB.Pools, files.MetalLBPoolDef(pool))
}
}

Expand Down
165 changes: 134 additions & 31 deletions cli/cmd/init_install_config_interactive_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@ package cmd

import (
"os"
"path/filepath"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/spf13/cobra"

"github.com/codesphere-cloud/oms/cli/cmd/util"
"github.com/codesphere-cloud/oms/internal/installer"
"github.com/codesphere-cloud/oms/internal/installer/files"
intutil "github.com/codesphere-cloud/oms/internal/util"
. "github.com/codesphere-cloud/oms/internal/util/testing"
)
Expand Down Expand Up @@ -85,42 +88,32 @@ var _ = Describe("Interactive profile usage", func() {
})

It("should generate valid config files with profile", func() {
configFile, err := os.CreateTemp("", "config-*.yaml")
Expect(err).NotTo(HaveOccurred())
defer func() { _ = os.Remove(configFile.Name()) }()
err = configFile.Close()
Expect(err).NotTo(HaveOccurred())

vaultFile, err := os.CreateTemp("", "vault-*.yaml")
Expect(err).NotTo(HaveOccurred())
defer func() { _ = os.Remove(vaultFile.Name()) }()
err = vaultFile.Close()
Expect(err).NotTo(HaveOccurred())
configPath, vaultPath := newTempConfigVaultPair()

c := &InitInstallConfigCmd{
Opts: &InitInstallConfigOpts{
GlobalOptions: &util.GlobalOptions{},
ConfigFile: configFile.Name(),
VaultFile: vaultFile.Name(),
ConfigFile: configPath,
VaultFile: vaultPath,
Profile: "dev",
Interactive: false, // Non-interactive to avoid stdin issues
},
FileWriter: intutil.NewFilesystemWriter(),
}

icg := installer.NewInstallConfigManager()
err = c.InitInstallConfig(icg)
err := c.InitInstallConfig(icg)
Expect(err).NotTo(HaveOccurred())

// Verify files were created
_, err = os.Stat(configFile.Name())
_, err = os.Stat(configPath)
Expect(err).NotTo(HaveOccurred())

_, err = os.Stat(vaultFile.Name())
_, err = os.Stat(vaultPath)
Expect(err).NotTo(HaveOccurred())

// Verify config content
err = icg.LoadInstallConfigFromFile(configFile.Name())
err = icg.LoadInstallConfigFromFile(configPath)
Expect(err).NotTo(HaveOccurred())

config := icg.GetInstallConfig()
Expand Down Expand Up @@ -171,23 +164,13 @@ var _ = Describe("Interactive profile usage", func() {
})

It("should still fail in non-interactive mode with validation errors", func() {
configFile, err := os.CreateTemp("", "config-*.yaml")
Expect(err).NotTo(HaveOccurred())
defer func() { _ = os.Remove(configFile.Name()) }()
err = configFile.Close()
Expect(err).NotTo(HaveOccurred())

vaultFile, err := os.CreateTemp("", "vault-*.yaml")
Expect(err).NotTo(HaveOccurred())
defer func() { _ = os.Remove(vaultFile.Name()) }()
err = vaultFile.Close()
Expect(err).NotTo(HaveOccurred())
configPath, vaultPath := newTempConfigVaultPair()

c := &InitInstallConfigCmd{
Opts: &InitInstallConfigOpts{
GlobalOptions: &util.GlobalOptions{},
ConfigFile: configFile.Name(),
VaultFile: vaultFile.Name(),
ConfigFile: configPath,
VaultFile: vaultPath,
Profile: "dev",
Interactive: false,
CodesphereOpenBaoUri: "not-a-valid-url",
Expand All @@ -197,9 +180,129 @@ var _ = Describe("Interactive profile usage", func() {

icg := installer.NewInstallConfigManager()

err = c.InitInstallConfig(icg)
err := c.InitInstallConfig(icg)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("configuration validation failed"))
})
})
})

var _ = Describe("Non-interactive install-config generation", func() {
DescribeTable("generates and validates the config for each profile",
func(profile string) {
configPath, vaultPath := newTempConfigVaultPair()

c := &InitInstallConfigCmd{
Opts: &InitInstallConfigOpts{
GlobalOptions: &util.GlobalOptions{},
ConfigFile: configPath,
VaultFile: vaultPath,
Profile: profile,
Interactive: false,
},
FileWriter: intutil.NewFilesystemWriter(),
}

icg := installer.NewInstallConfigManager()
err := c.InitInstallConfig(icg)
Expect(err).NotTo(HaveOccurred())

// Both files must have been written.
_, err = os.Stat(configPath)
Expect(err).NotTo(HaveOccurred())
_, err = os.Stat(vaultPath)
Expect(err).NotTo(HaveOccurred())

// The generated config must round-trip through the full load path and validate.
err = icg.LoadInstallConfigFromFile(configPath)
Expect(err).NotTo(HaveOccurred())
Expect(icg.ValidateInstallConfig()).To(BeEmpty())

// The --validate CLI path must pass. The vault is skipped because the freshly
// generated vault is plaintext and LoadVaultFromFile requires SOPS encryption.
validateCmd := &InitInstallConfigCmd{
Opts: &InitInstallConfigOpts{
GlobalOptions: &util.GlobalOptions{},
ConfigFile: configPath,
VaultFile: "",
ValidateOnly: true,
},
FileWriter: intutil.NewFilesystemWriter(),
}
validateIcg := installer.NewInstallConfigManager()
Expect(validateCmd.validateOnly(validateIcg)).To(Succeed())
},
Entry("dev profile", "dev"),
Entry("minimal profile", "minimal"),
Entry("production profile", "production"),
)
})

// newTempConfigVaultPair returns paths to fresh config/vault files inside an
// auto-cleaned temp directory.
func newTempConfigVaultPair() (configPath, vaultPath string) {
dir := GinkgoT().TempDir()
return filepath.Join(dir, "config.yaml"), filepath.Join(dir, "prod.vault.yaml")
}

var _ = Describe("Non-interactive Kubernetes CIDR flags", func() {
// buildCmd returns a command wired to a real cobra.Command registering the
// k8s flags (mirroring AddInitInstallConfigCmd), so Flags().Changed works.
buildCmd := func(opts *InitInstallConfigOpts) *InitInstallConfigCmd {
cmd := &cobra.Command{Use: "install-config"}
cmd.Flags().BoolVar(&opts.KubernetesManagedByCodesphere, "k8s-managed", true, "Use Codesphere-managed Kubernetes")
cmd.Flags().StringVar(&opts.KubernetesPodCIDR, "k8s-pod-cidr", "", "Pod CIDR (required when --k8s-managed=false)")
cmd.Flags().StringVar(&opts.KubernetesServiceCIDR, "k8s-service-cidr", "", "Service CIDR (required when --k8s-managed=false)")

return &InitInstallConfigCmd{cmd: cmd, Opts: opts}
}

It("applies --k8s-managed=false with both CIDRs", func() {
c := buildCmd(&InitInstallConfigOpts{GlobalOptions: &util.GlobalOptions{}})
Expect(c.cmd.Flags().Set("k8s-managed", "false")).To(Succeed())
Expect(c.cmd.Flags().Set("k8s-pod-cidr", "10.200.0.0/16")).To(Succeed())
Expect(c.cmd.Flags().Set("k8s-service-cidr", "10.100.0.0/16")).To(Succeed())

root := files.NewRootConfig()
config := &root
c.updateConfigFromOpts(config, &files.InstallVault{})

Expect(config.Kubernetes.ManagedByCodesphere).To(BeFalse())
Expect(config.Kubernetes.PodCIDR).To(Equal("10.200.0.0/16"))
Expect(config.Kubernetes.ServiceCIDR).To(Equal("10.100.0.0/16"))
})

It("does not override managed Kubernetes when --k8s-managed is not set", func() {
c := buildCmd(&InitInstallConfigOpts{GlobalOptions: &util.GlobalOptions{}})

// The flag is not passed, so its default (true) must not overwrite an
// existing value on the config, e.g. one loaded from disk.
root := files.NewRootConfig()
config := &root
config.Kubernetes.ManagedByCodesphere = false

c.updateConfigFromOpts(config, &files.InstallVault{})

Expect(config.Kubernetes.ManagedByCodesphere).To(BeFalse())
Expect(config.Kubernetes.PodCIDR).To(BeEmpty())
Expect(config.Kubernetes.ServiceCIDR).To(BeEmpty())
})

It("fails when --k8s-managed=false and the pod CIDR is missing", func() {
configPath, vaultPath := newTempConfigVaultPair()
opts := &InitInstallConfigOpts{
GlobalOptions: &util.GlobalOptions{},
ConfigFile: configPath,
VaultFile: vaultPath,
Profile: "dev",
Interactive: false,
}
c := buildCmd(opts)
Expect(c.cmd.Flags().Set("k8s-managed", "false")).To(Succeed())

icg := installer.NewInstallConfigManager()
err := c.InitInstallConfig(icg)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("pod CIDR is required"))
})
})
2 changes: 2 additions & 0 deletions docs/oms_init_install-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ $ oms init install-config --validate -c config.yaml --vault prod.vault.yaml
--interactive Enable interactive prompting (when true, other config flags are ignored) (default true)
--k8s-control-plane strings K8s control plane IPs (comma-separated)
--k8s-managed Use Codesphere-managed Kubernetes (default true)
--k8s-pod-cidr string Pod CIDR (required when --k8s-managed=false)
--k8s-service-cidr string Service CIDR (required when --k8s-managed=false)
--openbao-engine string Engine for OpenBao (default "cs-secrets-engine")
--openbao-password string Password for OpenBao authentication
--openbao-uri string URI for OpenBao (e.g., https://openbao.example.com)
Expand Down
Loading
Loading