diff --git a/api/README.md b/api/README.md new file mode 100644 index 000000000..63a3bba8e --- /dev/null +++ b/api/README.md @@ -0,0 +1,84 @@ +# Embedded Cluster API Package + +This package provides the core API functionality for the Embedded Cluster system. It handles installation, authentication, console access, and health monitoring of the cluster. + +## Package Structure + +### Root Level +The root directory contains the main API setup files and request handlers. + +### Subpackages + +#### `/controllers` +Contains the business logic for different API endpoints. Each controller package focuses on a specific domain of functionality (e.g., authentication, console, installation) and implements the core business logic for that domain. + +#### `/types` +Defines the core data structures and types used throughout the API. This includes: +- Request and response types +- Domain models +- Custom error types +- Shared interfaces + +#### `/pkg` +Contains shared utilities and helper packages that provide common functionality used across different parts of the API. This includes both general-purpose utilities and domain-specific helpers. + +#### `/client` +Provides a client library for interacting with the API. The client package implements a clean interface for making API calls and handling responses, making it easy to integrate with the API from other parts of the system. + +## Where to Add New Functionality + +1. **New API Endpoints**: + - Add route definitions in the root API setup + - Create corresponding controller in `/controllers` + - Define request/response types in `/types` + +2. **New Business Logic**: + - Place in appropriate controller under `/controllers` + - Share common logic in `/pkg` if used across multiple controllers + +3. **New Types/Models**: + - Add to `/types` directory + - Include validation and serialization methods + +4. **New Client Methods**: + - Add to appropriate file in `/client` + - Include corresponding tests + +5. **New Utilities**: + - Place in `/pkg/utils` if general purpose + - Create new subpackage under `/pkg` if domain-specific + +## Best Practices + +1. **Error Handling**: + - Use custom error types from `/types` + - Include proper error wrapping and context + - Maintain consistent error handling patterns + +2. **Testing**: + - Write unit tests for all new functionality + - Include integration tests for API endpoints + - Maintain high test coverage + +3. **Documentation**: + - Document all public types and functions + - Include examples for complex operations + - Keep README updated with new functionality + +4. **Logging**: + - Use the logging utilities from the root package + - Include appropriate log levels and context + - Follow consistent logging patterns + +## Architecture Decisions + +1. **Release Metadata Independence**: + - The EC API should not use the release metadata embedded into the EC binary (CLI) + - This design choice enables better testability and easier iteration in the development environment + - API components should be independently configurable and testable + +## Integration + +The API package is designed to be used as part of the larger Embedded Cluster system. It provides both HTTP endpoints for external access and a client library for internal use. + +For integration examples and usage patterns, refer to the integration tests in the `/integration` directory. diff --git a/api/api.go b/api/api.go index 77227e088..9a581e7c7 100644 --- a/api/api.go +++ b/api/api.go @@ -1,6 +1,7 @@ package api import ( + "encoding/json" "errors" "fmt" "net/http" @@ -110,10 +111,44 @@ func (a *API) RegisterRoutes(router *mux.Router) { consoleRouter.HandleFunc("/available-network-interfaces", a.getListAvailableNetworkInterfaces).Methods("GET") } -func handleError(w http.ResponseWriter, err error) { +func (a *API) JSON(w http.ResponseWriter, r *http.Request, code int, payload any) { + response, err := json.Marshal(payload) + if err != nil { + a.logError(r, err, "failed to encode response") + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(code) + w.Write(response) +} + +func (a *API) JSONError(w http.ResponseWriter, r *http.Request, err error) { var apiErr *types.APIError if !errors.As(err, &apiErr) { apiErr = types.NewInternalServerError(err) } - apiErr.JSON(w) + + response, err := json.Marshal(apiErr) + if err != nil { + a.logError(r, err, "failed to encode response") + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(apiErr.StatusCode) + w.Write(response) +} + +func (a *API) logError(r *http.Request, err error, args ...any) { + a.logger.WithFields(logrusFieldsFromRequest(r)).WithError(err).Error(args...) +} + +func logrusFieldsFromRequest(r *http.Request) logrus.Fields { + return logrus.Fields{ + "method": r.Method, + "path": r.URL.Path, + } } diff --git a/api/auth.go b/api/auth.go index a3e9da475..206b8bfe6 100644 --- a/api/auth.go +++ b/api/auth.go @@ -23,16 +23,14 @@ func (a *API) authMiddleware(next http.Handler) http.Handler { token := r.Header.Get("Authorization") if token == "" { err := errors.New("authorization header is required") - a.logger.WithFields(logrusFieldsFromRequest(r)).WithError(err). - Error("failed to authenticate") + a.logError(r, err, "failed to authenticate") types.NewUnauthorizedError(err).JSON(w) return } if !strings.HasPrefix(token, "Bearer ") { err := errors.New("authorization header must start with Bearer ") - a.logger.WithFields(logrusFieldsFromRequest(r)).WithError(err). - Error("failed to authenticate") + a.logError(r, err, "failed to authenticate") types.NewUnauthorizedError(err).JSON(w) return } @@ -41,8 +39,7 @@ func (a *API) authMiddleware(next http.Handler) http.Handler { err := a.authController.ValidateToken(r.Context(), token) if err != nil { - a.logger.WithFields(logrusFieldsFromRequest(r)).WithError(err). - Error("failed to validate token") + a.logError(r, err, "failed to validate token") types.NewUnauthorizedError(err).JSON(w) return } @@ -55,8 +52,7 @@ func (a *API) postAuthLogin(w http.ResponseWriter, r *http.Request) { var request AuthRequest err := json.NewDecoder(r.Body).Decode(&request) if err != nil { - a.logger.WithFields(logrusFieldsFromRequest(r)).WithError(err). - Error("failed to decode auth request") + a.logError(r, err, "failed to decode auth request") types.NewBadRequestError(err).JSON(w) return } @@ -68,8 +64,7 @@ func (a *API) postAuthLogin(w http.ResponseWriter, r *http.Request) { } if err != nil { - a.logger.WithFields(logrusFieldsFromRequest(r)).WithError(err). - Error("failed to authenticate") + a.logError(r, err, "failed to authenticate") types.NewInternalServerError(err).JSON(w) return } diff --git a/api/client/client.go b/api/client/client.go index 77210400a..76d1b3812 100644 --- a/api/client/client.go +++ b/api/client/client.go @@ -9,12 +9,6 @@ import ( "github.com/replicatedhq/embedded-cluster/api/types" ) -var defaultHTTPClient = &http.Client{ - Transport: &http.Transport{ - Proxy: nil, // This is a local client so no proxy is needed - }, -} - type Client interface { Login(password string) error GetInstall() (*types.Install, error) @@ -51,7 +45,7 @@ func New(apiURL string, opts ...ClientOption) Client { } if c.httpClient == nil { - c.httpClient = defaultHTTPClient + c.httpClient = http.DefaultClient } return c diff --git a/api/client/client_test.go b/api/client/client_test.go index bfda499aa..bbdf7bdf5 100644 --- a/api/client/client_test.go +++ b/api/client/client_test.go @@ -19,7 +19,7 @@ func TestNew(t *testing.T) { clientImpl, ok := c.(*client) assert.True(t, ok, "Expected c to be of type *client") assert.Equal(t, "http://example.com", clientImpl.apiURL) - assert.Equal(t, defaultHTTPClient, clientImpl.httpClient) + assert.Equal(t, http.DefaultClient, clientImpl.httpClient) assert.Empty(t, clientImpl.token) // Test with custom HTTP client diff --git a/api/console.go b/api/console.go index 8181f3a59..f2faf61fe 100644 --- a/api/console.go +++ b/api/console.go @@ -1,7 +1,6 @@ package api import ( - "encoding/json" "net/http" "github.com/replicatedhq/embedded-cluster/api/types" @@ -14,9 +13,8 @@ type getBrandingResponse struct { func (a *API) getBranding(w http.ResponseWriter, r *http.Request) { branding, err := a.consoleController.GetBranding() if err != nil { - a.logger.WithFields(logrusFieldsFromRequest(r)).WithError(err). - Error("failed to get branding") - handleError(w, err) + a.logError(r, err, "failed to get branding") + a.JSONError(w, r, err) return } @@ -24,13 +22,7 @@ func (a *API) getBranding(w http.ResponseWriter, r *http.Request) { Branding: branding, } - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - err = json.NewEncoder(w).Encode(response) - if err != nil { - a.logger.WithFields(logrusFieldsFromRequest(r)).WithError(err). - Error("failed to encode branding") - } + a.JSON(w, r, http.StatusOK, response) } type getListAvailableNetworkInterfacesResponse struct { @@ -40,9 +32,8 @@ type getListAvailableNetworkInterfacesResponse struct { func (a *API) getListAvailableNetworkInterfaces(w http.ResponseWriter, r *http.Request) { interfaces, err := a.consoleController.ListAvailableNetworkInterfaces() if err != nil { - a.logger.WithFields(logrusFieldsFromRequest(r)).WithError(err). - Error("failed to list available network interfaces") - handleError(w, err) + a.logError(r, err, "failed to list available network interfaces") + a.JSONError(w, r, err) return } @@ -54,11 +45,5 @@ func (a *API) getListAvailableNetworkInterfaces(w http.ResponseWriter, r *http.R NetworkInterfaces: interfaces, } - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - err = json.NewEncoder(w).Encode(response) - if err != nil { - a.logger.WithFields(logrusFieldsFromRequest(r)).WithError(err). - Error("failed to encode available network interfaces") - } + a.JSON(w, r, http.StatusOK, response) } diff --git a/api/controllers/auth/controller.go b/api/controllers/auth/controller.go index ede25d5cd..3594de793 100644 --- a/api/controllers/auth/controller.go +++ b/api/controllers/auth/controller.go @@ -13,7 +13,7 @@ type Controller interface { ValidateToken(ctx context.Context, token string) error } -var _ Controller = &AuthController{} +var _ Controller = (*AuthController)(nil) type AuthController struct { password string diff --git a/api/controllers/console/controller.go b/api/controllers/console/controller.go index 94d706969..61214b06b 100644 --- a/api/controllers/console/controller.go +++ b/api/controllers/console/controller.go @@ -13,6 +13,8 @@ type Controller interface { ListAvailableNetworkInterfaces() ([]string, error) } +var _ Controller = (*ConsoleController)(nil) + type ConsoleController struct { utils.NetUtils } diff --git a/api/controllers/install/controller.go b/api/controllers/install/controller.go index 93d1c5582..7891e5cdb 100644 --- a/api/controllers/install/controller.go +++ b/api/controllers/install/controller.go @@ -16,7 +16,7 @@ type Controller interface { ReadStatus(ctx context.Context) (*types.InstallationStatus, error) } -var _ Controller = &InstallController{} +var _ Controller = (*InstallController)(nil) type InstallController struct { installationManager installation.InstallationManager diff --git a/api/install.go b/api/install.go index e93e05683..7b86fc0db 100644 --- a/api/install.go +++ b/api/install.go @@ -5,44 +5,30 @@ import ( "net/http" "github.com/replicatedhq/embedded-cluster/api/types" - "github.com/sirupsen/logrus" ) -type InstallationConfigRequest struct { - DataDirectory string `json:"dataDirectory"` -} - func (a *API) getInstall(w http.ResponseWriter, r *http.Request) { install, err := a.installController.Get(r.Context()) if err != nil { - a.logger.WithFields(logrusFieldsFromRequest(r)).WithError(err). - Error("failed to get installation") - handleError(w, err) + a.logError(r, err, "failed to get installation") + a.JSONError(w, r, err) return } - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - err = json.NewEncoder(w).Encode(install) - if err != nil { - a.logger.WithFields(logrusFieldsFromRequest(r)).WithError(err). - Error("failed to encode installation") - } + a.JSON(w, r, http.StatusOK, install) } func (a *API) setInstallConfig(w http.ResponseWriter, r *http.Request) { var config types.InstallationConfig if err := json.NewDecoder(r.Body).Decode(&config); err != nil { - a.logger.WithFields(logrusFieldsFromRequest(r)).WithError(err). - Info("failed to decode installation config") + a.logError(r, err, "failed to decode installation config") types.NewBadRequestError(err).JSON(w) return } if err := a.installController.SetConfig(r.Context(), &config); err != nil { - a.logger.WithFields(logrusFieldsFromRequest(r)).WithError(err). - Error("failed to set installation config") - handleError(w, err) + a.logError(r, err, "failed to set installation config") + a.JSONError(w, r, err) return } @@ -57,16 +43,14 @@ func (a *API) setInstallConfig(w http.ResponseWriter, r *http.Request) { func (a *API) setInstallStatus(w http.ResponseWriter, r *http.Request) { var status types.InstallationStatus if err := json.NewDecoder(r.Body).Decode(&status); err != nil { - a.logger.WithFields(logrusFieldsFromRequest(r)).WithError(err). - Info("failed to decode installation status") + a.logError(r, err, "failed to decode installation status") types.NewBadRequestError(err).JSON(w) return } if err := a.installController.SetStatus(r.Context(), &status); err != nil { - a.logger.WithFields(logrusFieldsFromRequest(r)).WithError(err). - Error("failed to set installation status") - handleError(w, err) + a.logError(r, err, "failed to set installation status") + a.JSONError(w, r, err) return } @@ -76,24 +60,10 @@ func (a *API) setInstallStatus(w http.ResponseWriter, r *http.Request) { func (a *API) getInstallStatus(w http.ResponseWriter, r *http.Request) { status, err := a.installController.ReadStatus(r.Context()) if err != nil { - a.logger.WithFields(logrusFieldsFromRequest(r)).WithError(err). - Error("failed to get installation status") - handleError(w, err) + a.logError(r, err, "failed to get installation status") + a.JSONError(w, r, err) return } - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - err = json.NewEncoder(w).Encode(status) - if err != nil { - a.logger.WithFields(logrusFieldsFromRequest(r)).WithError(err). - Error("failed to encode installation status") - } -} - -func logrusFieldsFromRequest(r *http.Request) logrus.Fields { - return logrus.Fields{ - "method": r.Method, - "path": r.URL.Path, - } + a.JSON(w, r, http.StatusOK, status) } diff --git a/cmd/installer/cli/install.go b/cmd/installer/cli/install.go index 238c510a7..e9d5b412c 100644 --- a/cmd/installer/cli/install.go +++ b/cmd/installer/cli/install.go @@ -81,16 +81,20 @@ type InstallCmdFlags struct { configValues string networkInterface string - license *kotsv1beta1.License - proxy *ecv1beta1.ProxySpec - cidrCfg *newconfig.CIDRConfig - // guided UI flags managerPort int guidedUI bool - certFile string - keyFile string + tlsCertFile string + tlsKeyFile string hostname string + + // TODO: move to substruct + license *kotsv1beta1.License + proxy *ecv1beta1.ProxySpec + cidrCfg *newconfig.CIDRConfig + tlsCert tls.Certificate + tlsCertBytes []byte + tlsKeyBytes []byte } // InstallCmd returns a cobra command for installing the embedded cluster. @@ -213,8 +217,8 @@ func addInstallAdminConsoleFlags(cmd *cobra.Command, flags *InstallCmdFlags) err func addGuidedUIFlags(cmd *cobra.Command, flags *InstallCmdFlags) error { cmd.Flags().BoolVarP(&flags.guidedUI, "guided-ui", "g", false, "Run the installation in guided UI mode.") cmd.Flags().IntVar(&flags.managerPort, "manager-port", ecv1beta1.DefaultManagerPort, "Port on which the Manager will be served") - cmd.Flags().StringVar(&flags.certFile, "cert-file", "", "Path to the TLS certificate file") - cmd.Flags().StringVar(&flags.keyFile, "key-file", "", "Path to the TLS key file") + cmd.Flags().StringVar(&flags.tlsCertFile, "tls-cert", "", "Path to the TLS certificate file") + cmd.Flags().StringVar(&flags.tlsKeyFile, "tls-key", "", "Path to the TLS key file") cmd.Flags().StringVar(&flags.hostname, "hostname", "", "Hostname to use for TLS configuration") if err := cmd.Flags().MarkHidden("guided-ui"); err != nil { @@ -223,10 +227,10 @@ func addGuidedUIFlags(cmd *cobra.Command, flags *InstallCmdFlags) error { if err := cmd.Flags().MarkHidden("manager-port"); err != nil { return err } - if err := cmd.Flags().MarkHidden("cert-file"); err != nil { + if err := cmd.Flags().MarkHidden("tls-cert"); err != nil { return err } - if err := cmd.Flags().MarkHidden("key-file"); err != nil { + if err := cmd.Flags().MarkHidden("tls-key"); err != nil { return err } if err := cmd.Flags().MarkHidden("hostname"); err != nil { @@ -286,17 +290,33 @@ func preRunInstall(cmd *cobra.Command, flags *InstallCmdFlags) error { return fmt.Errorf("unable to list all valid IP addresses: %w", err) } - cert, err := tlsutils.GetCertificate(tlsutils.Config{ - CertFile: flags.certFile, - KeyFile: flags.keyFile, - Hostname: flags.hostname, - IPAddresses: ipAddresses, - }) - if err != nil { - return fmt.Errorf("get tls certificate: %w", err) + if flags.tlsCertFile != "" && flags.tlsKeyFile != "" { + cert, err := tls.LoadX509KeyPair(flags.tlsCertFile, flags.tlsKeyFile) + if err != nil { + return fmt.Errorf("load tls certificate: %w", err) + } + certData, err := os.ReadFile(flags.tlsCertFile) + if err != nil { + return fmt.Errorf("unable to read tls cert file: %w", err) + } + keyData, err := os.ReadFile(flags.tlsKeyFile) + if err != nil { + return fmt.Errorf("unable to read tls key file: %w", err) + } + flags.tlsCert = cert + flags.tlsCertBytes = certData + flags.tlsKeyBytes = keyData + } else { + cert, certData, keyData, err := tlsutils.GenerateCertificate(flags.hostname, ipAddresses) + if err != nil { + return fmt.Errorf("generate tls certificate: %w", err) + } + flags.tlsCert = cert + flags.tlsCertBytes = certData + flags.tlsKeyBytes = keyData } - if err := preRunInstallAPI(cmd.Context(), cert, flags.adminConsolePassword, flags.managerPort, configChan); err != nil { + if err := preRunInstallAPI(cmd.Context(), flags.tlsCert, flags.adminConsolePassword, flags.managerPort, configChan); err != nil { return fmt.Errorf("unable to start install API: %w", err) } @@ -580,6 +600,9 @@ func runInstall(ctx context.Context, name string, flags InstallCmdFlags, metrics Proxy: flags.proxy, HostCABundlePath: runtimeconfig.HostCABundlePath(), PrivateCAs: flags.privateCAs, + TLSCertBytes: flags.tlsCertBytes, + TLSKeyBytes: flags.tlsKeyBytes, + Hostname: flags.hostname, ServiceCIDR: flags.cidrCfg.ServiceCIDR, DisasterRecoveryEnabled: flags.license.Spec.IsDisasterRecoverySupported, IsMultiNodeEnabled: flags.license.Spec.IsEmbeddedClusterMultiNodeEnabled, @@ -627,10 +650,22 @@ func runInstall(ctx context.Context, name string, flags InstallCmdFlags, metrics } func markUIInstallComplete(password string, managerPort int) error { - apiClient := apiclient.New(fmt.Sprintf("http://localhost:%d", managerPort)) + httpClient := &http.Client{ + Transport: &http.Transport{ + Proxy: nil, // This is a local client so no proxy is needed + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + }, + } + apiClient := apiclient.New( + fmt.Sprintf("https://localhost:%d", managerPort), + apiclient.WithHTTPClient(httpClient), + ) if err := apiClient.Login(password); err != nil { return fmt.Errorf("unable to login: %w", err) } + _, err := apiClient.SetInstallStatus(apitypes.InstallationStatus{ State: apitypes.InstallationStateSucceeded, Description: "Install Complete", @@ -639,6 +674,7 @@ func markUIInstallComplete(password string, managerPort int) error { if err != nil { return fmt.Errorf("unable to set install status: %w", err) } + return nil } diff --git a/cmd/installer/cli/install_test.go b/cmd/installer/cli/install_test.go index 9d291d46f..908d52e56 100644 --- a/cmd/installer/cli/install_test.go +++ b/cmd/installer/cli/install_test.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "crypto/tls" + "crypto/x509" "fmt" "net" "net/http" @@ -550,9 +551,15 @@ func Test_runInstallAPI(t *testing.T) { logger := api.NewDiscardLogger() - cert, err := tlsutils.GetCertificate(tlsutils.Config{}) + _, port, err := net.SplitHostPort(listener.Addr().String()) require.NoError(t, err) + cert, _, _, err := tlsutils.GenerateCertificate("localhost", nil) + require.NoError(t, err) + + certPool := x509.NewCertPool() + certPool.AddCert(cert.Leaf) + go func() { err := runInstallAPI(ctx, listener, cert, logger, "password", nil) t.Logf("Install API exited with error: %v", err) @@ -560,16 +567,16 @@ func Test_runInstallAPI(t *testing.T) { }() t.Logf("Waiting for install API to start on %s", listener.Addr().String()) - err = waitForInstallAPI(ctx, listener.Addr().String()) + err = waitForInstallAPI(ctx, net.JoinHostPort("localhost", port)) assert.NoError(t, err) - url := "https://" + listener.Addr().String() + "/api/health" + url := "https://" + net.JoinHostPort("localhost", port) + "/api/health" t.Logf("Making request to %s", url) httpClient := http.Client{ Timeout: 2 * time.Second, Transport: &http.Transport{ TLSClientConfig: &tls.Config{ - InsecureSkipVerify: true, + RootCAs: certPool, }, }, } diff --git a/e2e/cluster/cmx/minio.go b/e2e/cluster/cmx/minio.go index 48eb0d7f1..843e5f3f7 100644 --- a/e2e/cluster/cmx/minio.go +++ b/e2e/cluster/cmx/minio.go @@ -25,25 +25,34 @@ func (c *Cluster) DeployMinio(node int) (*Minio, error) { return nil, fmt.Errorf("create minio directories: %v: %s: %s", err, stdout, stderr) } - // Install Go (only used for downloading minio and mc as the official mirrors get throttled in cmx) - stdout, stderr, err = c.RunCommandOnNode(node, []string{"curl", "-L", "https://go.dev/dl/go1.24.2.linux-amd64.tar.gz", "|", "sudo", "tar", "-C", "/usr/local", "-xz"}) - if err != nil { - return nil, fmt.Errorf("install go: %v: %s: %s", err, stdout, stderr) + // Download Minio binary + downloadCmd := []string{ + "curl", "-L", "https://dl.min.io/server/minio/release/linux-amd64/minio", + "-o", "/minio/bin/minio", } - - // Download minio binary - downloadEnvs := map[string]string{"GOBIN": "/minio/bin"} - downloadCmd := []string{"/usr/local/go/bin/go", "install", "github.com/minio/minio@v0.0.0-20250507153712-6d18dba9a20d"} - if stdout, stderr, err := c.RunCommandOnNode(node, downloadCmd, downloadEnvs); err != nil { + if stdout, stderr, err := c.RunCommandOnNode(node, downloadCmd); err != nil { return nil, fmt.Errorf("download minio: %v: %s: %s", err, stdout, stderr) } + // Make binary executable + if stdout, stderr, err := c.RunCommandOnNode(node, []string{"chmod", "+x", "/minio/bin/minio"}); err != nil { + return nil, fmt.Errorf("chmod minio: %v: %s: %s", err, stdout, stderr) + } + // Download mc binary - downloadCmd = []string{"/usr/local/go/bin/go", "install", "github.com/minio/mc@v0.0.0-20250506164133-19d87ba47505"} - if stdout, stderr, err := c.RunCommandOnNode(node, downloadCmd, downloadEnvs); err != nil { + downloadCmd = []string{ + "curl", "-L", "https://dl.min.io/client/mc/release/linux-amd64/mc", + "-o", "/minio/bin/mc", + } + if stdout, stderr, err := c.RunCommandOnNode(node, downloadCmd); err != nil { return nil, fmt.Errorf("download mc: %v: %s: %s", err, stdout, stderr) } + // Make binary executable + if stdout, stderr, err := c.RunCommandOnNode(node, []string{"chmod", "+x", "/minio/bin/mc"}); err != nil { + return nil, fmt.Errorf("chmod mc: %v: %s: %s", err, stdout, stderr) + } + // Generate credentials accessKey := uuid.New().String() secretKey := uuid.New().String() diff --git a/pkg-new/tlsutils/tls.go b/pkg-new/tlsutils/tls.go index e853f0b0c..6a83bffbf 100644 --- a/pkg-new/tlsutils/tls.go +++ b/pkg-new/tlsutils/tls.go @@ -6,7 +6,6 @@ import ( "net" "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" - "github.com/sirupsen/logrus" certutil "k8s.io/client-go/util/cert" ) @@ -30,29 +29,22 @@ type Config struct { IPAddresses []net.IP } -// GetCertificate returns a TLS certificate based on the provided configuration. -// If cert and key files are provided, it uses those. Otherwise, it generates a self-signed certificate. -func GetCertificate(cfg Config) (tls.Certificate, error) { - if cfg.CertFile != "" && cfg.KeyFile != "" { - logrus.Debugf("Using TLS configuration with cert file: %s and key file: %s", cfg.CertFile, cfg.KeyFile) - return tls.LoadX509KeyPair(cfg.CertFile, cfg.KeyFile) - } - - hostname, altNames := generateCertHostnames(cfg.Hostname) +// GenerateCertificate creates a new self-signed TLS certificate +func GenerateCertificate(hostname string, ipAddresses []net.IP) (tls.Certificate, []byte, []byte, error) { + hostname, altNames := generateCertHostnames(hostname) // Generate a new self-signed cert - certData, keyData, err := certutil.GenerateSelfSignedCertKey(hostname, cfg.IPAddresses, altNames) + certData, keyData, err := certutil.GenerateSelfSignedCertKey(hostname, ipAddresses, altNames) if err != nil { - return tls.Certificate{}, fmt.Errorf("generate self-signed cert: %w", err) + return tls.Certificate{}, nil, nil, fmt.Errorf("generate self-signed cert: %w", err) } cert, err := tls.X509KeyPair(certData, keyData) if err != nil { - return tls.Certificate{}, fmt.Errorf("create TLS certificate: %w", err) + return tls.Certificate{}, nil, nil, fmt.Errorf("create TLS certificate: %w", err) } - logrus.Debugf("Using self-signed TLS certificate for hostname: %s", hostname) - return cert, nil + return cert, certData, keyData, nil } // GetTLSConfig returns a TLS configuration with the provided certificate diff --git a/pkg/addons/adminconsole/adminconsole.go b/pkg/addons/adminconsole/adminconsole.go index 514773861..f6587cb7e 100644 --- a/pkg/addons/adminconsole/adminconsole.go +++ b/pkg/addons/adminconsole/adminconsole.go @@ -21,6 +21,9 @@ type AdminConsole struct { ServiceCIDR string Password string PrivateCAs []string + TLSCertBytes []byte + TLSKeyBytes []byte + Hostname string KotsInstaller KotsInstaller IsMultiNodeEnabled bool ReplicatedAppDomain string diff --git a/pkg/addons/adminconsole/install.go b/pkg/addons/adminconsole/install.go index a107422e6..6ec21a201 100644 --- a/pkg/addons/adminconsole/install.go +++ b/pkg/addons/adminconsole/install.go @@ -89,6 +89,10 @@ func (a *AdminConsole) createPreRequisites(ctx context.Context, kcli client.Clie return errors.Wrap(err, "create kots CA configmap") } + if err := a.createTLSSecret(ctx, kcli, namespace); err != nil { + return errors.Wrap(err, "create kots TLS secret") + } + if a.IsAirgap { registryIP, err := registry.GetRegistryClusterIP(a.ServiceCIDR) if err != nil { @@ -242,6 +246,54 @@ func (a *AdminConsole) createRegistrySecret(ctx context.Context, kcli client.Cli return nil } +func (a *AdminConsole) createTLSSecret(ctx context.Context, kcli client.Client, namespace string) error { + if len(a.TLSCertBytes) == 0 || len(a.TLSKeyBytes) == 0 { + return nil + } + + secret := &corev1.Secret{ + TypeMeta: metav1.TypeMeta{ + Kind: "Secret", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "kotsadm-tls", + Namespace: namespace, + Labels: map[string]string{ + "kots.io/kotsadm": "true", + "replicated.com/disaster-recovery": "infra", + "replicated.com/disaster-recovery-chart": "admin-console", + }, + Annotations: map[string]string{ + "acceptAnonymousUploads": "0", + }, + }, + Type: "kubernetes.io/tls", + Data: map[string][]byte{ + "tls.crt": a.TLSCertBytes, + "tls.key": a.TLSKeyBytes, + }, + StringData: map[string]string{ + "hostname": a.Hostname, + }, + } + + if a.DryRun { + b := bytes.NewBuffer(nil) + if err := serializer.Encode(secret, b); err != nil { + return errors.Wrap(err, "serialize TLS secret") + } + a.dryRunManifests = append(a.dryRunManifests, b.Bytes()) + } else { + err := kcli.Create(ctx, secret) + if err != nil { + return errors.Wrap(err, "create kotsadm-tls secret") + } + } + + return nil +} + func privateCAsToMap(privateCAs []string) (map[string]string, error) { cas := map[string]string{} diff --git a/pkg/addons/install.go b/pkg/addons/install.go index 6b08366c2..65b70632d 100644 --- a/pkg/addons/install.go +++ b/pkg/addons/install.go @@ -25,6 +25,9 @@ type InstallOptions struct { Proxy *ecv1beta1.ProxySpec HostCABundlePath string PrivateCAs []string + TLSCertBytes []byte + TLSKeyBytes []byte + Hostname string ServiceCIDR string DisasterRecoveryEnabled bool IsMultiNodeEnabled bool @@ -99,6 +102,9 @@ func getAddOnsForInstall(opts InstallOptions) []types.AddOn { ServiceCIDR: opts.ServiceCIDR, Password: opts.AdminConsolePwd, PrivateCAs: opts.PrivateCAs, + TLSCertBytes: opts.TLSCertBytes, + TLSKeyBytes: opts.TLSKeyBytes, + Hostname: opts.Hostname, KotsInstaller: opts.KotsInstaller, IsMultiNodeEnabled: opts.IsMultiNodeEnabled, ReplicatedAppDomain: domains.ReplicatedAppDomain,