Skip to content

Commit

Permalink
feat: add support for private custom image registries (#83)
Browse files Browse the repository at this point in the history
## Issue
#81

## Description
This PR adds support for cutom image registries. Under the hood, the
`Registry` struct will use Hauler if its `IsAirgapped` prop is set to
true.

Tested by configuring artifacts in a harbor registry as shown below and
installing validator with `InsecureSkipTLSVerify` set to `true` (and
passing in the CA cert) and `false`

<img width="1036" alt="Screenshot 2024-07-22 at 1 40 21 PM"
src="https://github.com/user-attachments/assets/05f1646b-eeb7-46f0-86ee-c6440024b11c">
  • Loading branch information
ahmad-ibra committed Jul 22, 2024
1 parent feb6360 commit ae91659
Show file tree
Hide file tree
Showing 17 changed files with 428 additions and 102 deletions.
2 changes: 1 addition & 1 deletion hack/validator.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ helmReleaseSecret:
exists: false
imageRegistry: quay.io/validator-labs
useFixedVersion: false
airgapConfig:
registryConfig:
enabled: false
kindConfig:
useKindCluster: true
Expand Down
49 changes: 35 additions & 14 deletions pkg/components/environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,32 +23,53 @@ type CACert struct {
Path string `yaml:"path"`
}

// Hauler represents the hauler configuration for air-gapped installs.
type Hauler struct {
// Registry represents the generic configuration for a registry.
// If IsAirgapped is true, a local Hauler registry is used.
type Registry struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
BasicAuth *BasicAuth `yaml:"basicAuth,omitempty"`
InsecureSkipTLSVerify bool `yaml:"insecureSkipTLSVerify"`
CACert *CACert `yaml:"caCert,omitempty"`
ReuseProxyCACert bool `yaml:"reuseProxyCACert,omitempty"`
BaseContentPath string `yaml:"baseContentPath"`
IsAirgapped bool `yaml:"isAirgapped"`
}

// Endpoint returns the base hauler registry URL.
func (h *Hauler) Endpoint() string {
return fmt.Sprintf("%s:%d", h.Host, h.Port)
// UnspecifiedPort is the value given to a Registry.Port when it is not specified.
const UnspecifiedPort = -1

// Endpoint returns the base registry URL.
func (r *Registry) Endpoint() string {
if r.Port != UnspecifiedPort {
return fmt.Sprintf("%s:%d", r.Host, r.Port)
}
return r.Host
}

// KindImage returns the image with the local hauler registry endpoint.
func (h *Hauler) KindImage(image string) string {
return fmt.Sprintf("localhost:%d/%s", h.Port, image)
// KindImage returns the image with the registry endpoint.
func (r *Registry) KindImage(image string) string {
if r.IsAirgapped {
return fmt.Sprintf("localhost:%d/%s", r.Port, image)
}
if r.BaseContentPath == "" {
return fmt.Sprintf("%s/%s", r.Endpoint(), image)
}
return fmt.Sprintf("%s/%s/%s", r.Endpoint(), r.BaseContentPath, image)
}

// ChartEndpoint returns the hauler chart repository URL.
func (h *Hauler) ChartEndpoint() string {
return fmt.Sprintf("oci://%s/hauler", h.Endpoint())
// ChartEndpoint returns the chart repository URL.
func (r *Registry) ChartEndpoint() string {
if r.IsAirgapped {
return fmt.Sprintf("oci://%s/hauler", r.Endpoint())
}
if r.BaseContentPath == "" {
return fmt.Sprintf("oci://%s/charts", r.Endpoint())
}
return fmt.Sprintf("oci://%s/%s/charts", r.Endpoint(), r.BaseContentPath)
}

// ImageEndpoint returns the hauler image repository URL.
func (h *Hauler) ImageEndpoint() string {
return fmt.Sprintf("%s/%s", h.Endpoint(), cfg.ValidatorImageRepository)
// ImageEndpoint returns the image repository URL.
func (r *Registry) ImageEndpoint() string {
return fmt.Sprintf("%s/%s", r.Endpoint(), cfg.ValidatorImageRepository)
}
14 changes: 7 additions & 7 deletions pkg/components/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ type ValidatorConfig struct {
ReleaseSecret *Secret `yaml:"helmReleaseSecret"`
KindConfig KindConfig `yaml:"kindConfig"`
Kubeconfig string `yaml:"kubeconfig"`
AirgapConfig *AirgapConfig `yaml:"airgapConfig"`
RegistryConfig *RegistryConfig `yaml:"registryConfig"`
SinkConfig *SinkConfig `yaml:"sinkConfig"`
ProxyConfig *ProxyConfig `yaml:"proxyConfig"`
ImageRegistry string `yaml:"imageRegistry"`
Expand All @@ -52,8 +52,8 @@ func NewValidatorConfig() *ValidatorConfig {
KindConfig: KindConfig{
UseKindCluster: false,
},
AirgapConfig: &AirgapConfig{
Hauler: &Hauler{
RegistryConfig: &RegistryConfig{
Registry: &Registry{
BasicAuth: &BasicAuth{},
CACert: &CACert{},
},
Expand Down Expand Up @@ -199,10 +199,10 @@ func (c *ValidatorConfig) encrypt() error {
return nil
}

// AirgapConfig represents the air-gapped configuration.
type AirgapConfig struct {
Enabled bool `yaml:"enabled"`
Hauler *Hauler `yaml:"hauler"`
// RegistryConfig represents the artifact registry configuration.
type RegistryConfig struct {
Enabled bool `yaml:"enabled"`
Registry *Registry `yaml:"registry"`
}

// KindConfig represents the kind configuration.
Expand Down
4 changes: 2 additions & 2 deletions pkg/config/versions.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ package config

// ValidatorChartVersions is a map of validator component names to their respective versions
var ValidatorChartVersions = map[string]string{
Validator: "v0.0.47",
Validator: "v0.0.48",
ValidatorPluginAws: "v0.1.1",
ValidatorPluginAzure: "v0.0.13",
ValidatorPluginNetwork: "v0.0.18",
ValidatorPluginNetwork: "v0.0.19",
ValidatorPluginOci: "v0.0.11",
ValidatorPluginVsphere: "v0.0.27",
}
185 changes: 168 additions & 17 deletions pkg/services/env_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,19 @@ package services

import (
"fmt"
"net/url"
"os"
"os/exec"
"strconv"
"time"

"github.com/pkg/errors"
"github.com/spectrocloud-labs/prompts-tui/prompts"

"github.com/validator-labs/validatorctl/pkg/components"
cfg "github.com/validator-labs/validatorctl/pkg/config"
log "github.com/validator-labs/validatorctl/pkg/logging"
exec_utils "github.com/validator-labs/validatorctl/pkg/utils/exec"
"github.com/validator-labs/validatorctl/pkg/utils/network"
)

Expand Down Expand Up @@ -55,7 +61,7 @@ func ReadProxyProps(e *components.Env) error {
}

// ReadHaulerProps prompts the user to configure hauler settings.
func ReadHaulerProps(h *components.Hauler, e *components.Env) error {
func ReadHaulerProps(h *components.Registry, e *components.Env) error {
var err error

// registry
Expand All @@ -74,26 +80,11 @@ func ReadHaulerProps(h *components.Hauler, e *components.Env) error {
return err
}

// basic auth
if h.BasicAuth == nil {
h.BasicAuth = &components.BasicAuth{}
}
h.BasicAuth.Username, h.BasicAuth.Password, err = prompts.ReadBasicCreds(
"Username", "Password", h.BasicAuth.Username, h.BasicAuth.Password, true, false,
)
err = readAuthTLSProps(h)
if err != nil {
return err
}

// tls verification
h.InsecureSkipTLSVerify, err = prompts.ReadBool("Allow Insecure Connection (Bypass x509 Verification)", true)
if err != nil {
return err
}
if h.InsecureSkipTLSVerify {
return nil
}

// ca cert
if e.ProxyCACert.Path != "" {
h.ReuseProxyCACert, err = prompts.ReadBool("Reuse proxy CA cert for Hauler registry", true)
Expand Down Expand Up @@ -123,3 +114,163 @@ func ReadHaulerProps(h *components.Hauler, e *components.Env) error {

return nil
}

// ReadRegistryProps prompts the user to configure custom private registry settings.
func ReadRegistryProps(r *components.Registry, e *components.Env) error {
ociURL, err := prompts.ReadURL(
"Registry Endpoint", "", "Invalid Registry Endpoint. A scheme is required, e.g.: 'https://'.", false,
)
if err != nil {
return err
}

parsedURL, err := url.Parse(ociURL)
if err != nil {
return err
}
r.Host = parsedURL.Hostname()
port := parsedURL.Port()
if port == "" {
r.Port = components.UnspecifiedPort
} else {
r.Port, err = strconv.Atoi(port)
if err != nil {
return err
}
}

baseContentPath, err := prompts.ReadText("Registry Base Content Path", "", true, -1)
if err != nil {
return err
}
r.BaseContentPath = baseContentPath

err = readAuthTLSProps(r)
if err != nil {
return err
}

// ca cert
if e.ProxyCACert.Path != "" {
r.ReuseProxyCACert, err = prompts.ReadBool("Reuse proxy CA cert for OCI registry", true)
if err != nil {
return err
}
}
if r.CACert == nil {
r.CACert = &components.CACert{}
}
if r.ReuseProxyCACert {
r.CACert = e.ProxyCACert
} else {
caCertPath, caCertName, caCertData, err := prompts.ReadCACert("OCI registry CA certificate filepath", r.CACert.Path, "")
if err != nil {
return err
}

if caCertPath != "" {
r.CACert.Data = string(caCertData)
r.CACert.Name = caCertName
r.CACert.Path = caCertPath
}
}

return ensureDockerOciCaConfig(r.CACert, r.Host)
}

func readAuthTLSProps(r *components.Registry) error {
var err error

// basic auth
if r.BasicAuth == nil {
r.BasicAuth = &components.BasicAuth{}
}
r.BasicAuth.Username, r.BasicAuth.Password, err = prompts.ReadBasicCreds(
"Username", "Password", r.BasicAuth.Username, r.BasicAuth.Password, true, false,
)
if err != nil {
return err
}

// tls verification
r.InsecureSkipTLSVerify, err = prompts.ReadBool("Allow Insecure Connection (Bypass x509 Verification)", true)
if err != nil {
return err
}
if r.InsecureSkipTLSVerify {
return nil
}

return nil
}

func ensureDockerOciCaConfig(caCert *components.CACert, endpoint string) error {
// TODO: mock this function properly
if os.Getenv("IS_TEST") == "true" {
return nil
}

dockerOciCaDir := fmt.Sprintf("/etc/docker/certs.d/%s", endpoint)
dockerOciCaPath := fmt.Sprintf("%s/%s", dockerOciCaDir, caCert.Name)

if _, err := os.Stat(dockerOciCaPath); err != nil {
log.InfoCLI("OCI CA configuration for Docker not found")

if err := ensureDockerCACertDir(dockerOciCaDir); err != nil {
return err
}

cmd := exec.Command("sudo", "cp", caCert.Path, dockerOciCaPath) //#nosec G204
_, stderr, err := exec_utils.Execute(true, cmd)
if err != nil {
log.InfoCLI("Failed to configure OCI CA certificate")
return errors.Wrap(err, stderr)
}
log.InfoCLI("Copied OCA CA certificate from %s to %s", caCert.Path, dockerOciCaPath)

log.InfoCLI("Restarting Docker...")
cmd = exec.Command("sudo", "systemctl", "daemon-reload")
_, stderr, err = exec_utils.Execute(true, cmd)
if err != nil {
log.InfoCLI("Failed to reload systemd manager configuration")
log.InfoCLI("Please execute 'sudo systemctl daemon-reload' manually and retry")
return errors.Wrap(err, stderr)
}

cmd = exec.Command("sudo", "systemctl", "restart", "docker")
_, stderr, err = exec_utils.Execute(true, cmd)
if err != nil {
log.InfoCLI("Failed to restart Docker")
log.InfoCLI("Please execute 'sudo systemctl restart docker' manually and retry")
return errors.Wrap(err, stderr)
}
log.InfoCLI("Configured OCA CA certificate for Docker")
}
return nil
}

func ensureDockerCACertDir(path string) error {
fi, err := os.Stat(path)
if err != nil {
return createDockerCACertDir(path)
}
if !fi.IsDir() {
cmd := exec.Command("sudo", "rm", "-f", path) //#nosec G204
_, stderr, err := exec_utils.Execute(true, cmd)
if err != nil {
return errors.Wrapf(err, stderr)
}
return createDockerCACertDir(path)
}
return nil
}

func createDockerCACertDir(path string) error {
cmd := exec.Command("sudo", "mkdir", "-p", path) //#nosec G204
_, stderr, err := exec_utils.Execute(true, cmd)
if err != nil {
return errors.Wrapf(err, stderr)
}
log.InfoCLI("Created Docker OCI CA certificate directory: %s", path)
return nil
}
2 changes: 1 addition & 1 deletion pkg/services/validator/aws_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import (
)

var awsDummyConfig = &components.ValidatorConfig{
AirgapConfig: &components.AirgapConfig{
RegistryConfig: &components.RegistryConfig{
Enabled: false,
},
AWSPlugin: &components.AWSPluginConfig{
Expand Down
12 changes: 4 additions & 8 deletions pkg/services/validator/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@ func readHelmRelease(name string, k8sClient kubernetes.Interface, vc *components
r.Chart.Name = name
rs.Name = fmt.Sprintf("validator-helm-release-%s", name)

if vc.AirgapConfig.Enabled {
r.Chart.Repository = vc.AirgapConfig.Hauler.ChartEndpoint()
log.InfoCLI("Using local Hauler repository: %s", vc.AirgapConfig.Hauler.ChartEndpoint())
if vc.RegistryConfig.Enabled {
r.Chart.Repository = vc.RegistryConfig.Registry.ChartEndpoint()
log.InfoCLI("Using helm repository: %s", vc.RegistryConfig.Registry.ChartEndpoint())
} else {
r.Chart.Repository, err = prompts.ReadText(fmt.Sprintf("%s Helm repository", name), defaultRepo, false, -1)
if err != nil {
Expand Down Expand Up @@ -72,11 +72,7 @@ func readHelmRelease(name string, k8sClient kubernetes.Interface, vc *components
}
}

if err := readHelmCredentials(r, rs, k8sClient, vc); err != nil {
return err
}

return nil
return readHelmCredentials(r, rs, k8sClient, vc)
}

func readHelmCredentials(r *vapi.HelmRelease, rs *components.Secret, k8sClient kubernetes.Interface, vc *components.ValidatorConfig) error {
Expand Down
2 changes: 1 addition & 1 deletion pkg/services/validator/network_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import (
)

var networkDummyConfig = &components.ValidatorConfig{
AirgapConfig: &components.AirgapConfig{
RegistryConfig: &components.RegistryConfig{
Enabled: false,
},
NetworkPlugin: &components.NetworkPluginConfig{
Expand Down
Loading

0 comments on commit ae91659

Please sign in to comment.