Skip to content
Merged
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: 5 additions & 0 deletions roxctl/central/db/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import (
"time"

"github.com/spf13/cobra"
"github.com/stackrox/rox/pkg/env"
"github.com/stackrox/rox/roxctl/central/db/backup"
"github.com/stackrox/rox/roxctl/central/db/generate"
"github.com/stackrox/rox/roxctl/central/db/restore"
"github.com/stackrox/rox/roxctl/common/environment"
"github.com/stackrox/rox/roxctl/common/flags"
Expand All @@ -17,6 +19,9 @@ func Command(cliEnvironment environment.Environment) *cobra.Command {
}
c.AddCommand(backup.Command(cliEnvironment))
c.AddCommand(restore.V2Command(cliEnvironment))
if env.PostgresDatastoreEnabled.BooleanSetting() {
c.AddCommand(generate.Command(cliEnvironment))
}
flags.AddTimeoutWithDefault(c, 1*time.Hour)
return c
}
161 changes: 161 additions & 0 deletions roxctl/central/db/generate/generate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
package generate

import (
"net/http"
"os"
"time"

"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/stackrox/rox/pkg/buildinfo"
"github.com/stackrox/rox/pkg/errorhelpers"
"github.com/stackrox/rox/pkg/images/defaults"
"github.com/stackrox/rox/pkg/mtls"
"github.com/stackrox/rox/pkg/renderer"
"github.com/stackrox/rox/pkg/roxctl"
"github.com/stackrox/rox/pkg/set"
"github.com/stackrox/rox/pkg/zip"
"github.com/stackrox/rox/roxctl/common"
"github.com/stackrox/rox/roxctl/common/environment"
"github.com/stackrox/rox/roxctl/common/flags"
"github.com/stackrox/rox/roxctl/common/logger"
"github.com/stackrox/rox/roxctl/common/zipdownload"
)

const (
centralDBCertGeneratePath = "/api/extensions/certgen/centraldb"
)

var (
centralDBCertBundle = set.NewFrozenStringSet(mtls.CACertFileName, mtls.CentralDBCertFileName, mtls.CentralDBKeyFileName)
)

type generateCommand struct {
// Properties that are bound to cobra flags.
config *renderer.Config

// Properties that are injected or constructed.
env environment.Environment

// timeout to make Central API call
timeout time.Duration
}

// Command represents the generate command.
func Command(cliEnvironment environment.Environment) *cobra.Command {
cmd := &generateCommand{config: &cfg, env: cliEnvironment}

c := &cobra.Command{
Use: "generate",
Hidden: true,
}

if !buildinfo.ReleaseBuild {
flags.AddHelmChartDebugSetting(c)
}
c.PersistentFlags().BoolVar(&cmd.config.EnablePodSecurityPolicies, "enable-pod-security-policies", true, "Create PodSecurityPolicy resources (for pre-v1.25 Kubernetes)")
c.PersistentPreRunE = func(*cobra.Command, []string) error {
cmd.construct(c)
return cmd.populateMTLS()
}

c.AddCommand(k8s(cliEnvironment))
c.AddCommand(openshift(cliEnvironment))

return c
}

func (cmd *generateCommand) populateMTLS() error {
envLogger := cmd.env.Logger()
envLogger.InfofLn("Populating Central DB Certificate from bundle...")
fileMap, err := zipdownload.GetZipFiles(zipdownload.GetZipOptions{
Path: centralDBCertGeneratePath,
Method: http.MethodPost,
Timeout: cmd.timeout,
BundleType: "central-db",
ExpandZip: true,
}, envLogger)
if err != nil {
return err
}
err = verifyCentralDBBundleFiles(fileMap)
if err != nil {
return err
}
cmd.config.SecretsByteMap = map[string][]byte{
"ca.pem": fileMap[mtls.CACertFileName].Content,
"central-db-cert.pem": fileMap[mtls.CentralDBCertFileName].Content,
"central-db-key.pem": fileMap[mtls.CentralDBKeyFileName].Content,
"central-db-password": []byte(renderer.CreatePassword()),
}
return nil
}

func (cmd *generateCommand) construct(c *cobra.Command) {
cmd.timeout = flags.Timeout(c)
}

func generateBundleWrapper(config renderer.Config) (*zip.Wrapper, error) {
rendered, err := render(config)
if err != nil {
return nil, err
}

wrapper := zip.NewWrapper()
wrapper.AddFiles(rendered...)
return wrapper, errors.Wrap(err, "could not get scanner bundle")
}

func outputZip(envLogger logger.Logger, config renderer.Config) error {
envLogger.InfofLn("Generating Central DB bundle...")
common.LogInfoPsp(envLogger, config.EnablePodSecurityPolicies)
wrapper, err := generateBundleWrapper(config)
if err != nil {
return err
}
if roxctl.InMainImage() {
bytes, err := wrapper.Zip()
if err != nil {
return errors.Wrap(err, "error generating zip file")
}
_, err = os.Stdout.Write(bytes)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We made a huge effort to not use os in roxctl. Please use logger instead. With os.Stdout there is no way to unit test it. Please consider adding unit tests.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dhaus67 Why it was not caught by forbidigo linter? 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the output of zipped files. I cannot use logger.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use InputOutput from env

if err != nil {
return errors.Wrap(err, "couldn't write zip file")
}
return nil
}
outputPath, err := wrapper.Directory(config.OutputDir)
if err != nil {
return errors.Wrap(err, "error generating directory for Central output")
}
envLogger.InfofLn("Wrote central bundle to %q", outputPath)
return nil
}

func render(config renderer.Config) ([]*zip.File, error) {
flavor, err := defaults.GetImageFlavorByName(config.K8sConfig.ImageFlavorName, buildinfo.ReleaseBuild)
if err != nil {
return nil, common.ErrInvalidCommandOption.CausedByf("'--%s': %v", flags.ImageDefaultsFlagName, err)
}

return renderer.RenderCentralDBOnly(config, flavor)
}

func verifyCentralDBBundleFiles(fm map[string]*zip.File) error {
var errs errorhelpers.ErrorList

checkList := centralDBCertBundle.Unfreeze()
for k, v := range fm {
if len(v.Content) == 0 {
errs.AddError(errors.Errorf("empty file in Central DB certificate bundle: %s", v.Name))
}
if !centralDBCertBundle.Contains(k) {
errs.AddError(errors.Errorf("unexpected file in Central DB certificate bundle: %s", k))
}
checkList.Remove(k)
}
if checkList.Cardinality() != 0 {
errs.AddError(errors.Errorf("missing file(s) in Central DB certificate bundle %s", checkList.ElementsString(",")))
}
return errs.ToError()
}
89 changes: 89 additions & 0 deletions roxctl/central/db/generate/k8s.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package generate

import (
"fmt"

"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/stackrox/rox/generated/storage"
"github.com/stackrox/rox/pkg/errox"
"github.com/stackrox/rox/pkg/renderer"
"github.com/stackrox/rox/pkg/roxctl"
"github.com/stackrox/rox/roxctl/common"
"github.com/stackrox/rox/roxctl/common/environment"
"github.com/stackrox/rox/roxctl/common/flags"
"github.com/stackrox/rox/roxctl/common/util"
)

const (
defaultCentralDBBundle = "central-db-bundle"
)

func orchestratorCommand(shortName, longName string) *cobra.Command {
c := &cobra.Command{
Use: shortName,
Short: shortName,
Long: longName,
RunE: util.RunENoArgs(func(*cobra.Command) error {
return errox.InvalidArgs.New("storage type must be specified")
}),
}
if !roxctl.InMainImage() {
c.PersistentFlags().Var(common.NewOutputDir(&cfg.OutputDir, defaultCentralDBBundle), "output-dir", "the directory to output the deployment bundle to")
}
return c
}

func k8sBasedOrchestrator(cliEnvironment environment.Environment, k8sConfig *renderer.K8sConfig, shortName, longName string, getClusterType func() (storage.ClusterType, error)) *cobra.Command {
c := orchestratorCommand(shortName, longName)
c.PersistentPreRunE = func(*cobra.Command, []string) error {
clusterType, err := getClusterType()
if err != nil {
return errors.Wrap(err, "determining cluster type")
}
cfg.K8sConfig = k8sConfig
cfg.ClusterType = clusterType
return nil
}

c.AddCommand(externalVolume(cliEnvironment))
c.AddCommand(hostPathVolume(cliEnvironment))
c.AddCommand(noVolume(cliEnvironment))

// Adds k8s specific flags
flags.AddImageDefaults(c.PersistentFlags(), &k8sConfig.ImageFlavorName)

defaultImageHelp := fmt.Sprintf("(if unset, a default will be used according to --%s)", flags.ImageDefaultsFlagName)
c.PersistentFlags().StringVarP(&k8sConfig.CentralDBImage, flags.FlagNameCentralDBImage, "", "", "central-db image to use"+defaultImageHelp)
k8sConfig.EnableCentralDB = true

return c
}

func newK8sConfig() *renderer.K8sConfig {
return &renderer.K8sConfig{}
}

func k8s(cliEnvironment environment.Environment) *cobra.Command {
k8sConfig := newK8sConfig()
return k8sBasedOrchestrator(cliEnvironment, k8sConfig, "k8s", "Kubernetes", func() (storage.ClusterType, error) { return storage.ClusterType_KUBERNETES_CLUSTER, nil })
}

func openshift(cliEnvironment environment.Environment) *cobra.Command {
k8sConfig := newK8sConfig()

var openshiftVersion int
c := k8sBasedOrchestrator(cliEnvironment, k8sConfig, "openshift", "Openshift", func() (storage.ClusterType, error) {
switch openshiftVersion {
case 3:
return storage.ClusterType_OPENSHIFT_CLUSTER, nil
case 4:
return storage.ClusterType_OPENSHIFT4_CLUSTER, nil
default:
return 0, errors.Errorf("invalid OpenShift version %d, supported values are '3' and '4'", openshiftVersion)
}
})

c.PersistentFlags().IntVar(&openshiftVersion, "openshift-version", 3, "the OpenShift major version (3 or 4) to deploy on")
return c
}
30 changes: 30 additions & 0 deletions roxctl/central/db/generate/validate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package generate

import (
"github.com/stackrox/rox/pkg/errox"
"github.com/stackrox/rox/pkg/renderer"
)

var (
cfg renderer.Config
)

func validateConfig(c *renderer.Config) error {
if c.HostPath == nil {
return nil
}
return validateHostPathInstance(c.HostPath.DB)
}

func validateHostPathInstance(instance *renderer.HostPathPersistenceInstance) error {
if instance == nil {
return nil
}
if instance.HostPath == "" {
return errox.InvalidArgs.New("non-empty HostPath must be specified")
}
if (instance.NodeSelectorKey == "") != (instance.NodeSelectorValue == "") {
return errox.InvalidArgs.New("both node selector key and node selector value must be specified when using a hostpath")
}
return nil
}
66 changes: 66 additions & 0 deletions roxctl/central/db/generate/volumes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package generate

import (
"fmt"

"github.com/spf13/cobra"
"github.com/stackrox/rox/pkg/renderer"
"github.com/stackrox/rox/roxctl/common/environment"
)

func volumeCommand(name string) *cobra.Command {
return &cobra.Command{
Use: name,
Short: fmt.Sprintf("adds a %s", name),
Long: fmt.Sprintf(`adds a %s external volume to Central DB`, name),
}
}

func externalVolume(cliEnvironment environment.Environment) *cobra.Command {
external := &renderer.ExternalPersistence{
DB: &renderer.ExternalPersistenceInstance{},
}
c := volumeCommand("pvc")
c.RunE = func(c *cobra.Command, args []string) error {
cfg.External = external
if err := validateConfig(&cfg); err != nil {
return err
}
return outputZip(cliEnvironment.Logger(), cfg)
}
c.Flags().StringVarP(&external.DB.Name, "name", "", "central-db", "external volume name for Central DB")
c.Flags().StringVarP(&external.DB.StorageClass, "storage-class", "", "", "storage class name for Central DB (optional if you have a default StorageClass configured)")
c.Flags().Uint32VarP(&external.DB.Size, "size", "", 100, "external volume size in Gi for Central DB")
return c
}

func noVolume(cliEnvironment environment.Environment) *cobra.Command {
c := volumeCommand("none")
c.RunE = func(c *cobra.Command, args []string) error {
if err := validateConfig(&cfg); err != nil {
return err
}
return outputZip(cliEnvironment.Logger(), cfg)
}
c.Hidden = true
return c
}

func hostPathVolume(cliEnvironment environment.Environment) *cobra.Command {
hostpath := &renderer.HostPathPersistence{
DB: &renderer.HostPathPersistenceInstance{},
}
c := volumeCommand("hostpath")
c.RunE = func(c *cobra.Command, args []string) error {
cfg.HostPath = hostpath
if err := validateConfig(&cfg); err != nil {
return err
}
return outputZip(cliEnvironment.Logger(), cfg)
}
c.Flags().StringVarP(&hostpath.DB.HostPath, "hostpath", "", "/var/lib/stackrox-central", "path on the host")
c.Flags().StringVarP(&hostpath.DB.NodeSelectorKey, "node-selector-key", "", "", "node selector key (e.g. kubernetes.io/hostname)")
c.Flags().StringVarP(&hostpath.DB.NodeSelectorValue, "node-selector-value", "", "", "node selector value")

return c
}
4 changes: 3 additions & 1 deletion roxctl/central/generate/k8s.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@ import (
"github.com/stackrox/rox/pkg/renderer"
"github.com/stackrox/rox/pkg/roxctl"
"github.com/stackrox/rox/pkg/utils"
"github.com/stackrox/rox/roxctl/common"
"github.com/stackrox/rox/roxctl/common/environment"
"github.com/stackrox/rox/roxctl/common/flags"
"github.com/stackrox/rox/roxctl/common/util"
)

const (
noteOpenShift3xCompatibilityMode = `NOTE: Deployment files are generated in OpenShift 3.x compatibility mode. Set the --openshift-version flag to 3 to suppress this note, or to 4 take advantage of OpenShift 4.x features.`
defaultBundlePath = "central-bundle"
)

type flagsWrapper struct {
Expand Down Expand Up @@ -67,7 +69,7 @@ func orchestratorCommand(shortName, longName string) *cobra.Command {
}),
}
if !roxctl.InMainImage() {
c.PersistentFlags().Var(newOutputDir(&cfg.OutputDir), "output-dir", "the directory to output the deployment bundle to")
c.PersistentFlags().Var(common.NewOutputDir(&cfg.OutputDir, defaultBundlePath), "output-dir", "the directory to output the deployment bundle to")
}
return c
}
Expand Down
Loading