diff --git a/ci/utils.go b/ci/utils.go index 954ec1a..cde501a 100644 --- a/ci/utils.go +++ b/ci/utils.go @@ -58,7 +58,6 @@ func (m *HarborSatellite) Service( AsService() } - // builds given component from source func (m *HarborSatellite) build(source *dagger.Directory, component string) *dagger.Directory { fmt.Printf("Building %s\n", component) diff --git a/cmd/container_runtime/containerd.go b/cmd/container_runtime/containerd.go new file mode 100644 index 0000000..f044110 --- /dev/null +++ b/cmd/container_runtime/containerd.go @@ -0,0 +1,140 @@ +package runtime + +import ( + "context" + "fmt" + "os" + "path/filepath" + "strings" + + "container-registry.com/harbor-satellite/internal/config" + "container-registry.com/harbor-satellite/internal/utils" + "container-registry.com/harbor-satellite/logger" + "container-registry.com/harbor-satellite/registry" + toml "github.com/pelletier/go-toml" + "github.com/rs/zerolog" + "github.com/spf13/cobra" +) + +const ( + ContainerDCertPath = "/etc/containerd/certs.d" + DefaultGeneratedTomlName = "config.toml" + ContainerdRuntime = "containerd" + DefaultContainerdConfigPath = "/etc/containerd/config.toml" + DefaultConfigVersion = 2 +) + +type ContainerdController interface { + Load(ctx context.Context, log *zerolog.Logger) (*registry.DefaultZotConfig, error) + Generate(ctx context.Context, configPath string, log *zerolog.Logger) error +} + +var DefaultContainerDGenPath string + +func init() { + cwd, err := os.Getwd() + if err != nil { + fmt.Printf("Error getting current working directory: %v\n", err) + DefaultContainerDGenPath = "/runtime/containerd" + if _, err := os.Stat(DefaultContainerDGenPath); os.IsNotExist(err) { + err := os.MkdirAll(DefaultContainerDGenPath, os.ModePerm) + if err != nil { + fmt.Printf("Error creating default directory: %v\n", err) + } + } + } else { + DefaultContainerDGenPath = filepath.Join(cwd, "runtime/containerd") + } +} + +func NewContainerdCommand() *cobra.Command { + var generateConfig bool + var defaultZotConfig *registry.DefaultZotConfig + var containerdConfigPath string + var containerDCertPath string + + containerdCmd := &cobra.Command{ + Use: "containerd", + Short: "Creates the config file for the containerd runtime to fetch the images from the local repository", + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + return SetupContainerRuntimeCommand(cmd, &defaultZotConfig, DefaultContainerDGenPath) + }, + RunE: func(cmd *cobra.Command, args []string) error { + log := logger.FromContext(cmd.Context()) + sourceRegistry := config.GetRemoteRegistryURL() + satelliteHostConfig := NewSatelliteHostConfig(defaultZotConfig.GetLocalRegistryURL(), sourceRegistry) + if generateConfig { + log.Info().Msg("Generating containerd config file for containerd ...") + log.Info().Msgf("Fetching containerd config from path: %s", containerdConfigPath) + err := GenerateContainerdHostConfig(containerDCertPath, DefaultContainerDGenPath, log, *satelliteHostConfig) + if err != nil { + log.Err(err).Msg("Error generating containerd config") + return fmt.Errorf("could not generate containerd config: %w", err) + } + return GenerateContainerdConfig(defaultZotConfig, log, containerdConfigPath, containerDCertPath) + } + return nil + }, + } + + containerdCmd.Flags().BoolVarP(&generateConfig, "gen", "g", false, "Generate the containerd config file") + containerdCmd.PersistentFlags().StringVarP(&containerdConfigPath, "path", "p", DefaultContainerdConfigPath, "Path to the containerd config file of the container runtime") + containerdCmd.PersistentFlags().StringVarP(&containerDCertPath, "cert-path", "c", ContainerDCertPath, "Path to the containerd cert directory") + containerdCmd.AddCommand(NewReadConfigCommand(ContainerdRuntime)) + return containerdCmd +} + +// GenerateContainerdConfig generates the containerd config file for the containerd runtime +// It takes the zot config a logger and the containerd config path +// It reads the containerd config file and adds the local registry to the config file +func GenerateContainerdConfig(defaultZotConfig *registry.DefaultZotConfig, log *zerolog.Logger, containerdConfigPath, containerdCertPath string) error { + // First Read the present config file at the configPath + data, err := utils.ReadFile(containerdConfigPath, false) + if err != nil { + log.Err(err).Msg("Error reading config file") + return fmt.Errorf("could not read config file: %w", err) + } + // Now we marshal the data into the containerd config + containerdConfig := &ContainerdConfigToml{} + err = toml.Unmarshal(data, containerdConfig) + if err != nil { + log.Err(err).Msg("Error unmarshalling config") + return fmt.Errorf("could not unmarshal config: %w", err) + } + // Add the certs.d path to the config + if containerdConfig.Plugins.Cri.Registry.ConfigPath == "" { + containerdConfig.Plugins.Cri.Registry.ConfigPath = containerdCertPath + } + // Set default version + if containerdConfig.Version == 0 { + containerdConfig.Version = DefaultConfigVersion + } + // if config disabled plugins container cri then remove it + if len(containerdConfig.DisabledPlugins) > 0 { + filteredPlugins := make([]string, 0, len(containerdConfig.DisabledPlugins)) + for _, plugin := range containerdConfig.DisabledPlugins { + if plugin != "cri" { + filteredPlugins = append(filteredPlugins, plugin) + } + } + containerdConfig.DisabledPlugins = filteredPlugins + } + // ToDo: Find a way to remove the unwanted configuration added to the config file while marshalling + pathToWrite := filepath.Join(DefaultContainerDGenPath, DefaultGeneratedTomlName) + log.Info().Msgf("Writing the containerd config to path: %s", pathToWrite) + // Now we write the config to the file + data, err = toml.Marshal(containerdConfig) + dataStr := string(data) + dataStr = strings.Replace(dataStr, "[plugins]\n", "", 1) + data = []byte(dataStr) + if err != nil { + log.Err(err).Msg("Error marshalling config") + return fmt.Errorf("could not marshal config: %w", err) + } + err = utils.WriteFile(pathToWrite, data) + if err != nil { + log.Err(err).Msg("Error writing config to file") + return fmt.Errorf("could not write config to file: %w", err) + } + return nil +} diff --git a/cmd/container_runtime/containerd_config.go b/cmd/container_runtime/containerd_config.go new file mode 100644 index 0000000..0148aa7 --- /dev/null +++ b/cmd/container_runtime/containerd_config.go @@ -0,0 +1,169 @@ +package runtime + +// ContainerdConfigToml provides containerd configuration data for the server +type ContainerdConfigToml struct { + // Version of the config file + Version int `toml:"version,omitempty"` + // Root is the path to a directory where containerd will store persistent data + Root string `toml:"root,omitempty"` + // State is the path to a directory where containerd will store transient data + State string `toml:"state,omitempty"` + // TempDir is the path to a directory where to place containerd temporary files + TempDir string `toml:"temp,omitempty"` + // PluginDir is the directory for dynamic plugins to be stored + // + // Deprecated: Please use proxy or binary external plugins. + PluginDir string `toml:"plugin_dir,omitempty"` + // GRPC configuration settings + GRPC GRPCConfig `toml:"grpc,omitempty"` + // TTRPC configuration settings + TTRPC TTRPCConfig `toml:"ttrpc,omitempty"` + // Debug and profiling settings + Debug Debug `toml:"debug,omitempty"` + // Metrics and monitoring settings + Metrics MetricsConfig `toml:"metrics,omitempty"` + // DisabledPlugins are IDs of plugins to disable. Disabled plugins won't be + // initialized and started. + // DisabledPlugins must use a fully qualified plugin URI. + DisabledPlugins []string `toml:"disabled_plugins,omitempty"` + // RequiredPlugins are IDs of required plugins. Containerd exits if any + // required plugin doesn't exist or fails to be initialized or started. + // RequiredPlugins must use a fully qualified plugin URI. + RequiredPlugins []string `toml:"required_plugins,omitempty"` + // Plugins provides plugin specific configuration for the initialization of a plugin + Plugins PluginsConfig `toml:"plugins,omitempty"` + // OOMScore adjust the containerd's oom score + OOMScore int `toml:"oom_score,omitempty"` + // Cgroup specifies cgroup information for the containerd daemon process + Cgroup CgroupConfig `toml:"cgroup,omitempty"` + // ProxyPlugins configures plugins which are communicated to over GRPC + ProxyPlugins map[string]ProxyPlugin `toml:"proxy_plugins,omitempty"` + // Timeouts specified as a duration + Timeouts map[string]string `toml:"timeouts,omitempty"` + // Imports are additional file path list to config files that can overwrite main config file fields + Imports []string `toml:"imports,omitempty"` + // StreamProcessors configuration + StreamProcessors map[string]StreamProcessor `toml:"stream_processors,omitempty"` +} + +type StreamProcessor struct { + // Accepts specific media-types + Accepts []string `toml:"accepts,omitempty"` + // Returns the media-type + Returns string `toml:"returns,omitempty"` + // Path or name of the binary + Path string `toml:"path"` + // Args to the binary + Args []string `toml:"args,omitempty"` + // Environment variables for the binary + Env []string `toml:"env,omitempty"` +} + +type GRPCConfig struct { + Address string `toml:"address"` + TCPAddress string `toml:"tcp_address,omitempty"` + TCPTLSCA string `toml:"tcp_tls_ca,omitempty"` + TCPTLSCert string `toml:"tcp_tls_cert,omitempty"` + TCPTLSKey string `toml:"tcp_tls_key,omitempty"` + UID int `toml:"uid,omitempty"` + GID int `toml:"gid,omitempty"` + MaxRecvMsgSize int `toml:"max_recv_message_size,omitempty"` + MaxSendMsgSize int `toml:"max_send_message_size,omitempty"` +} + +// TTRPCConfig provides TTRPC configuration for the socket +type TTRPCConfig struct { + Address string `toml:"address"` + UID int `toml:"uid,omitempty"` + GID int `toml:"gid,omitempty"` +} + +// Debug provides debug configuration +type Debug struct { + Address string `toml:"address,omitempty"` + UID int `toml:"uid,omitempty"` + GID int `toml:"gid,omitempty"` + Level string `toml:"level,omitempty"` + // Format represents the logging format. Supported values are 'text' and 'json'. + Format string `toml:"format,omitempty"` +} + +// MetricsConfig provides metrics configuration +type MetricsConfig struct { + Address string `toml:"address,omitempty"` + GRPCHistogram bool `toml:"grpc_histogram,omitempty"` +} + +// CgroupConfig provides cgroup configuration +type CgroupConfig struct { + Path string `toml:"path,omitempty"` +} + +// ProxyPlugin provides a proxy plugin configuration +type ProxyPlugin struct { + Type string `toml:"type"` + Address string `toml:"address"` + Platform string `toml:"platform,omitempty"` + Exports map[string]string `toml:"exports,omitempty"` + Capabilities []string `toml:"capabilities,omitempty"` +} + +type PluginsConfig struct { + Cri CriConfig `toml:"io.containerd.grpc.v1.cri,omitempty"` + Cgroups MonitorConfig `toml:"io.containerd.monitor.v1.cgroups,omitempty"` + LinuxRuntime interface{} `toml:"io.containerd.runtime.v1.linux,omitempty"` + Scheduler GCSchedulerConfig `toml:"io.containerd.gc.v1.scheduler,omitempty"` + Bolt interface{} `toml:"io.containerd.metadata.v1.bolt,omitempty"` + Task RuntimeV2TaskConfig `toml:"io.containerd.runtime.v2.task,omitempty"` + Opt interface{} `toml:"io.containerd.internal.v1.opt,omitempty"` + Restart interface{} `toml:"io.containerd.internal.v1.restart,omitempty"` + Tracing interface{} `toml:"io.containerd.internal.v1.tracing,omitempty"` + Otlp interface{} `toml:"io.containerd.tracing.processor.v1.otlp,omitempty"` + Aufs interface{} `toml:"io.containerd.snapshotter.v1.aufs,omitempty"` + Btrfs interface{} `toml:"io.containerd.snapshotter.v1.btrfs,omitempty"` + Devmapper interface{} `toml:"io.containerd.snapshotter.v1.devmapper,omitempty"` + Native interface{} `toml:"io.containerd.snapshotter.v1.native,omitempty"` + Overlayfs interface{} `toml:"io.containerd.snapshotter.v1.overlayfs,omitempty"` + Zfs interface{} `toml:"io.containerd.snapshotter.v1.zfs,omitempty"` +} + +type MonitorConfig struct { + NoPrometheus bool `toml:"no_prometheus,omitempty"` +} + +type GCSchedulerConfig struct { + PauseThreshold float64 `toml:"pause_threshold,omitempty"` + DeletionThreshold int `toml:"deletion_threshold,omitempty"` + MutationThreshold int `toml:"mutation_threshold,omitempty"` + ScheduleDelay string `toml:"schedule_delay,omitempty"` + StartupDelay string `toml:"startup_delay,omitempty"` +} + +type RuntimeV2TaskConfig struct { + Platforms []string `toml:"platforms,omitempty"` + SchedCore bool `toml:"sched_core,omitempty"` +} + +type CriConfig struct { + Containerd CriContainerdConfig `toml:"containerd,omitempty"` + Registry RegistryConfig `toml:"registry,omitempty"` +} + +type CriContainerdConfig struct { + DefaultRuntimeName string `toml:"default_runtime_name,omitempty"` + Runtimes map[string]RuntimeConfig `toml:"runtimes,omitempty"` +} + +type RuntimeConfig struct { + PrivilegedWithoutHostDevices bool `toml:"privileged_without_host_devices,omitempty"` + RuntimeType string `toml:"runtime_type"` + Options RuntimeOptions `toml:"options,omitempty"` +} + +type RuntimeOptions struct { + BinaryName string `toml:"BinaryName,omitempty"` +} + +type RegistryConfig struct { + ConfigPath string `toml:"config_path,omitempty"` +} \ No newline at end of file diff --git a/cmd/container_runtime/crio.go b/cmd/container_runtime/crio.go new file mode 100644 index 0000000..f150c44 --- /dev/null +++ b/cmd/container_runtime/crio.go @@ -0,0 +1,172 @@ +package runtime + +import ( + "fmt" + "os" + "path/filepath" + + "container-registry.com/harbor-satellite/internal/config" + "container-registry.com/harbor-satellite/internal/utils" + "container-registry.com/harbor-satellite/logger" + "container-registry.com/harbor-satellite/registry" + "github.com/pelletier/go-toml/v2" + "github.com/rs/zerolog" + "github.com/spf13/cobra" +) + +const ( + DefaultCrioRegistryConfigPath = "/etc/containers/registries.conf.d/crio.conf" +) + +var DefaultCrioGenPath string + +func init() { + cwd, err := os.Getwd() + if err != nil { + fmt.Printf("Error getting current working directory: %v\n", err) + if _, err := os.Stat(DefaultCrioGenPath); os.IsNotExist(err) { + DefaultCrioGenPath = "runtime/crio" + err := os.MkdirAll(DefaultCrioGenPath, os.ModePerm) + if err != nil { + fmt.Printf("Error creating default directory: %v\n", err) + } + } + } else { + DefaultCrioGenPath = filepath.Join(cwd, "runtime/crio") + } +} + +func NewCrioCommand() *cobra.Command { + var defaultZotConfig *registry.DefaultZotConfig + var generateConfig bool + var crioConfigPath string + + crioCmd := &cobra.Command{ + Use: "crio", + Short: "Creates the config file for the crio runtime to fetch the images from the local repository", + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + return SetupContainerRuntimeCommand(cmd, &defaultZotConfig, DefaultCrioGenPath) + }, + RunE: func(cmd *cobra.Command, args []string) error { + log := logger.FromContext(cmd.Context()) + if generateConfig { + log.Info().Msg("Generating the config file for crio ...") + log.Info().Msgf("Fetching crio registry config file form path: %s", crioConfigPath) + // Generate the config file + err := GenerateCrioRegistryConfig(defaultZotConfig, crioConfigPath, log) + if err != nil { + log.Err(err).Msg("Error generating crio registry config") + return err + } + } + return nil + }, + } + crioCmd.Flags().BoolVarP(&generateConfig, "gen", "g", false, "Generate the config file") + crioCmd.PersistentFlags().StringVarP(&crioConfigPath, "config", "c", DefaultCrioRegistryConfigPath, "Path to the crio registry config file") + return crioCmd +} + +func GenerateCrioRegistryConfig(defaultZotConfig *registry.DefaultZotConfig, crioConfigPath string, log *zerolog.Logger) error { + // Read the current crio registry config file + data, err := utils.ReadFile(crioConfigPath, false) + if err != nil { + return fmt.Errorf("could not read crio registry config file: %w", err) + } + var crioRegistryConfig CriORegistryConfig + err = toml.Unmarshal(data, &crioRegistryConfig) + if err != nil { + log.Err(err).Msg("Error unmarshalling crio registry config") + return fmt.Errorf("could not unmarshal crio registry config: %w", err) + } + // Update the crio registry config file + // - Add the local registry to the unqualified search registries if not already present + var found bool = false + var localRegistry string = utils.FormatRegistryURL(defaultZotConfig.GetLocalRegistryURL()) + for _, registry := range crioRegistryConfig.UnqualifiedSearchRegistries { + if registry == localRegistry { + found = true + break + } + } + if !found { + crioRegistryConfig.UnqualifiedSearchRegistries = append(crioRegistryConfig.UnqualifiedSearchRegistries, localRegistry) + } + // Now range over the registries and find if there is a registry with the prefix satellite + // If there is a registry with the prefix satellite, update the location to the local registry + found = false + for _, registries := range crioRegistryConfig.Registries { + if registries.Prefix == "satellite.io" { + found = true + if registries.Location == "" { + registries.Location = DockerURL + } + // Add the local registry to the first position in the mirrors + mirror := Mirror{ + Location: localRegistry, + Insecure: config.UseUnsecure(), + } + registries.Mirrors = append([]Mirror{mirror}, registries.Mirrors...) + } + } + if !found { + // Add the satellite registry to the registries + registry := Registry{ + Prefix: "satellite.io", + Location: DockerURL, + Mirrors: []Mirror{ + { + Location: localRegistry, + Insecure: config.UseUnsecure(), + }, + }, + } + crioRegistryConfig.Registries = append(crioRegistryConfig.Registries, registry) + } + // Now marshal the updated crio registry config + updatedData, err := toml.Marshal(crioRegistryConfig) + if err != nil { + log.Err(err).Msg("Error marshalling crio registry config") + return fmt.Errorf("could not marshal crio registry config: %w", err) + } + // Write the updated crio registry config to the file + pathToWrite := filepath.Join(DefaultCrioGenPath, "crio.conf") + log.Info().Msgf("Writing the crio registry config to path: %s", pathToWrite) + err = utils.WriteFile(pathToWrite, updatedData) + if err != nil { + log.Err(err).Msg("Error writing crio registry config") + return fmt.Errorf("could not write crio registry config: %w", err) + } + log.Info().Msg("Successfully wrote the crio registry config") + return nil +} + +func SetupContainerRuntimeCommand(cmd *cobra.Command, defaultZotConfig **registry.DefaultZotConfig, defaultGenPath string) error { + var err error + err = config.InitConfig() + if err != nil { + return fmt.Errorf("could not initialize config: %w", err) + } + utils.SetupContextForCommand(cmd) + log := logger.FromContext(cmd.Context()) + + if config.GetOwnRegistry() { + log.Info().Msg("Using own registry for config generation") + address, err := utils.ValidateRegistryAddress(config.GetOwnRegistryAdr(), config.GetOwnRegistryPort()) + if err != nil { + log.Err(err).Msg("Error validating registry address") + return err + } + log.Info().Msgf("Registry address validated: %s", address) + (*defaultZotConfig).HTTP.Address = config.GetOwnRegistryAdr() + (*defaultZotConfig).HTTP.Port = config.GetOwnRegistryPort() + } else { + log.Info().Msg("Using default registry for config generation") + *defaultZotConfig, err = registry.ReadConfig(config.GetZotConfigPath()) + if err != nil || *defaultZotConfig == nil { + return fmt.Errorf("could not read config: %w", err) + } + log.Info().Msgf("Default config read successfully: %v", (*defaultZotConfig).HTTP.Address+":"+(*defaultZotConfig).HTTP.Port) + } + return utils.CreateRuntimeDirectory(defaultGenPath) +} diff --git a/cmd/container_runtime/crio_config.go b/cmd/container_runtime/crio_config.go new file mode 100644 index 0000000..8575a5a --- /dev/null +++ b/cmd/container_runtime/crio_config.go @@ -0,0 +1,51 @@ +package runtime + +// CriORegistryConfig represents the overall configuration for container image registries. +type CriORegistryConfig struct { + // UnqualifiedSearchRegistries is an array of host[:port] registries to try + // when pulling an unqualified image, in the specified order. + UnqualifiedSearchRegistries []string `toml:"unqualified-search-registries,omitempty"` + + // Registries is a list of registry configurations, each defining the behavior for a specific prefix or namespace. + Registries []Registry `toml:"registry,omitempty"` +} + +// Registry represents a specific registry configuration. +type Registry struct { + // Prefix is used to choose the relevant [[registry]] TOML table. + // Only the table with the longest match for the input image name + // (considering namespace/repo/tag/digest separators) is used. + // If this field is missing, it defaults to the value of Location. + // Example: "example.com/foo" + Prefix string `toml:"prefix,omitempty"` + + // Insecure allows unencrypted HTTP as well as TLS connections with untrusted certificates + // if set to true. This should only be enabled for trusted registries to avoid security risks. + Insecure bool `toml:"insecure,omitempty"` + + // Blocked, if set to true, prevents pulling images with matching names from this registry. + // This can be used to blacklist certain registries. + Blocked bool `toml:"blocked,omitempty"` + + // Location specifies the physical location of the "prefix"-rooted namespace. + // By default, this is equal to "prefix". It can be empty for wildcarded prefixes (e.g., "*.example.com"), + // in which case the input reference is used as-is without modification. + // Example: "internal-registry-for-example.com/bar" + Location string `toml:"location,omitempty"` + + // Mirrors is an array of potential mirror locations for the "prefix"-rooted namespace. + // Mirrors are attempted in the specified order; the first reachable mirror containing the image + // is used. If no mirror has the image, the primary location or the unmodified user-specified reference is tried last. + Mirrors []Mirror `toml:"mirror,omitempty"` +} + +// Mirror represents a mirror registry configuration. +type Mirror struct { + // Location specifies the address of the mirror. The mirror will be used if it contains the image. + // Example: "example-mirror-0.local/mirror-for-foo" + Location string `toml:"location,omitempty"` + + // Insecure allows access to the mirror over unencrypted HTTP or with untrusted TLS certificates + // if set to true. This should be used cautiously. + Insecure bool `toml:"insecure,omitempty"` +} diff --git a/cmd/container_runtime/host.go b/cmd/container_runtime/host.go new file mode 100644 index 0000000..27b1e49 --- /dev/null +++ b/cmd/container_runtime/host.go @@ -0,0 +1,111 @@ +package runtime + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "container-registry.com/harbor-satellite/internal/config" + "container-registry.com/harbor-satellite/internal/utils" + "github.com/pelletier/go-toml/v2" + "github.com/rs/zerolog" +) + +const ( + SatelliteConfigPath = "satellite.io" + HostToml = "host_gen.toml" + DefaultTomlConfigPath = "_default" + DockerURL = "docker.io" +) + +type ContainerdHostConfig struct { + Server string `toml:"server,omitempty"` + Host map[string]HostConfig `toml:"host,omitempty"` +} + +type HostConfig struct { + Capabilities []string `toml:"capabilities,omitempty"` + CA interface{} `toml:"ca,omitempty"` + Client interface{} `toml:"client,omitempty"` + SkipVerify bool `toml:"skip_verify,omitempty"` + Header map[string][]string `toml:"header,omitempty"` + OverridePath bool `toml:"override_path,omitempty"` +} + +type SatelliteHostConfig struct { + LocalRegistry string + SourceRegistry string +} + +func NewSatelliteHostConfig(localRegistry, sourceRegistry string) *SatelliteHostConfig { + return &SatelliteHostConfig{ + LocalRegistry: localRegistry, + SourceRegistry: sourceRegistry, + } +} + +// GenerateContainerdHostConfig generates the host.toml file for containerd docker.io and also create a default config.toml file +func GenerateContainerdHostConfig(containerdCertPath, genPath string, log *zerolog.Logger, satelliteHostConfig SatelliteHostConfig) error { + mirrorGenPath := fmt.Sprintf("%s/%s", genPath, SatelliteConfigPath) + err := utils.CreateRuntimeDirectory(mirrorGenPath) + if err != nil { + log.Err(err).Msgf("Error creating the directory: %s", mirrorGenPath) + return fmt.Errorf("error creating the directory: %v", err) + } + satelliteHubHostConfigPath := fmt.Sprintf("%s/%s/%s", containerdCertPath, SatelliteConfigPath, HostToml) + var satelliteContainerdHostConfig ContainerdHostConfig + + // Read the `satellite/host.toml` file if present + data, err := utils.ReadFile(satelliteHubHostConfigPath, false) + if err != nil { + if os.IsNotExist(err) { + log.Warn().Msgf("The satellite/host.toml file does not exist at path: %s", satelliteHubHostConfigPath) + } else { + return fmt.Errorf("error reading the satellite/host.toml file: %v", err) + } + } + err = toml.Unmarshal(data, &satelliteContainerdHostConfig) + if err != nil { + log.Err(err).Msgf("Error unmarshalling the satellite/host.toml file at path: %s", satelliteHubHostConfigPath) + return fmt.Errorf("error unmarshalling the satellite/host.toml file: %v", err) + } + satelliteHostConfigToAdd := ContainerdHostConfig{ + Host: map[string]HostConfig{ + satelliteHostConfig.LocalRegistry: { + Capabilities: []string{"pull", "push", "resolve"}, + SkipVerify: config.UseUnsecure(), + }, + }, + } + + if satelliteContainerdHostConfig.Server == "" { + satelliteContainerdHostConfig.Server = DockerURL + } + if satelliteContainerdHostConfig.Host == nil { + satelliteContainerdHostConfig.Host = satelliteHostConfigToAdd.Host + } else { + for key, value := range satelliteContainerdHostConfig.Host { + satelliteHostConfigToAdd.Host[key] = value + } + satelliteContainerdHostConfig.Host = satelliteHostConfigToAdd.Host + } + + pathTOWrite := filepath.Join(mirrorGenPath, HostToml) + log.Info().Msgf("Writing the host.toml file at path: %s", pathTOWrite) + hostData, err := toml.Marshal(satelliteContainerdHostConfig) + if err != nil { + log.Err(err).Msg("Error marshalling the host.toml file") + return fmt.Errorf("error marshalling the host.toml file: %v", err) + } + hostStr := string(hostData) + hostStr = strings.Replace(hostStr, "[host]\n", "", 1) + hostData = []byte(hostStr) + err = utils.WriteFile(pathTOWrite, hostData) + if err != nil { + log.Err(err).Msg("Error writing the host.toml file") + return fmt.Errorf("error writing the host.toml file: %v", err) + } + log.Info().Msg("Successfully wrote the host.toml file") + return nil +} diff --git a/cmd/container_runtime/read_config.go b/cmd/container_runtime/read_config.go new file mode 100644 index 0000000..94af5df --- /dev/null +++ b/cmd/container_runtime/read_config.go @@ -0,0 +1,36 @@ +package runtime + +import ( + "fmt" + + "container-registry.com/harbor-satellite/internal/config" + "container-registry.com/harbor-satellite/internal/utils" + "container-registry.com/harbor-satellite/logger" + "github.com/spf13/cobra" +) + +func NewReadConfigCommand(runtime string) *cobra.Command { + readContainerdConfig := &cobra.Command{ + Use: "read", + Short: fmt.Sprintf("Reads the config file for the %s runtime", runtime), + PersistentPreRun: func(cmd *cobra.Command, args []string) { + utils.SetupContextForCommand(cmd) + config.InitConfig() + }, + RunE: func(cmd *cobra.Command, args []string) error { + //Parse the flags + path, err := cmd.Flags().GetString("path") + if err != nil { + return fmt.Errorf("error reading the path flag: %v", err) + } + log := logger.FromContext(cmd.Context()) + log.Info().Msgf("Reading the containerd config file from path: %s", path) + _, err = utils.ReadFile(path, true) + if err != nil { + return fmt.Errorf("error reading the containerd config file: %v", err) + } + return nil + }, + } + return readContainerdConfig +} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..66a2c7b --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,105 @@ +package cmd + +import ( + "context" + + runtime "container-registry.com/harbor-satellite/cmd/container_runtime" + "container-registry.com/harbor-satellite/internal/config" + "container-registry.com/harbor-satellite/internal/satellite" + "container-registry.com/harbor-satellite/internal/scheduler" + "container-registry.com/harbor-satellite/internal/server" + "container-registry.com/harbor-satellite/internal/utils" + "container-registry.com/harbor-satellite/logger" + "github.com/rs/zerolog" + "github.com/spf13/cobra" + "golang.org/x/sync/errgroup" +) + +func NewRootCommand() *cobra.Command { + rootCmd := &cobra.Command{ + Use: "harbor-satellite", + Short: "Harbor Satellite is a tool to replicate images from source registry to Harbor registry", + PersistentPreRun: func(cmd *cobra.Command, args []string) { + config.InitConfig() + }, + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + ctx, cancel := utils.SetupContext(ctx) + ctx = logger.AddLoggerToContext(ctx, config.GetLogLevel()) + return run(ctx, cancel) + }, + } + rootCmd.AddCommand(runtime.NewContainerdCommand()) + rootCmd.AddCommand(runtime.NewCrioCommand()) + return rootCmd +} + +func Execute() error { + return NewRootCommand().Execute() +} + +func run(ctx context.Context, cancel context.CancelFunc) error { + g, ctx := errgroup.WithContext(ctx) + log := logger.FromContext(ctx) + + // Set up router and app + app := setupServerApp(ctx, log) + app.SetupRoutes() + app.SetupServer(g) + + // Handle registry setup + if err := handleRegistrySetup(g, log, cancel); err != nil { + return err + } + scheduler := scheduler.NewBasicScheduler(ctx) + ctx = context.WithValue(ctx, scheduler.GetSchedulerKey(), scheduler) + err := scheduler.Start() + if err != nil { + log.Error().Err(err).Msg("Error starting scheduler") + return err + } + + satelliteService := satellite.NewSatellite(ctx, scheduler.GetSchedulerKey()) + + g.Go(func() error { + return satelliteService.Run(ctx) + }) + + log.Info().Msg("Startup complete 🚀") + return g.Wait() +} + +func setupServerApp(ctx context.Context, log *zerolog.Logger) *server.App { + router := server.NewDefaultRouter("/api/v1") + router.Use(server.LoggingMiddleware) + + return server.NewApp( + router, + ctx, + log, + &server.MetricsRegistrar{}, + &server.DebugRegistrar{}, + &satellite.SatelliteRegistrar{}, + ) +} + +func handleRegistrySetup(g *errgroup.Group, log *zerolog.Logger, cancel context.CancelFunc) error { + if config.GetOwnRegistry() { + if err := utils.HandleOwnRegistry(); err != nil { + log.Error().Err(err).Msg("Error handling own registry") + return err + } + } else { + log.Info().Msg("Launching default registry") + g.Go(func() error { + if err := utils.LaunchDefaultZotRegistry(); err != nil { + log.Error().Err(err).Msg("Error launching default registry") + cancel() + return err + } + cancel() + return nil + }) + } + return nil +} diff --git a/go.mod b/go.mod index 8829be4..db2ae17 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,12 @@ require ( github.com/stretchr/testify v1.9.0 ) -require golang.org/x/oauth2 v0.22.0 // indirect +require ( + github.com/blang/semver/v4 v4.0.0 // indirect + github.com/pelletier/go-toml v1.9.5 // indirect + golang.org/x/oauth2 v0.22.0 // indirect + k8s.io/cri-api v0.27.1 // indirect +) require ( cloud.google.com/go v0.112.1 // indirect @@ -124,7 +129,7 @@ require ( github.com/cheggaaa/pb/v3 v3.1.5 // indirect github.com/cloudflare/circl v1.3.7 // indirect github.com/containerd/cgroups/v3 v3.0.3 // indirect - github.com/containerd/containerd v1.7.18 // indirect + github.com/containerd/containerd v1.7.18 github.com/containerd/continuity v0.4.3 // indirect github.com/containerd/errdefs v0.1.0 // indirect github.com/containerd/fifo v1.1.0 // indirect @@ -315,7 +320,7 @@ require ( github.com/owenrumney/go-sarif/v2 v2.3.1 // indirect github.com/owenrumney/squealer v1.2.2 // indirect github.com/package-url/packageurl-go v0.1.3 // indirect - github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect @@ -353,7 +358,7 @@ require ( github.com/spdx/tools-golang v0.5.4 // indirect github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.6.0 // indirect - github.com/spf13/cobra v1.8.0 // indirect + github.com/spf13/cobra v1.8.0 github.com/spf13/pflag v1.0.5 // indirect github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6 // indirect github.com/stretchr/objx v0.5.2 // indirect diff --git a/go.sum b/go.sum index 3102b75..8b1e1ba 100644 --- a/go.sum +++ b/go.sum @@ -451,6 +451,8 @@ github.com/bitnami/go-version v0.0.0-20231130084017-bb00604d650c h1:C4UZIaS+HAw+ github.com/bitnami/go-version v0.0.0-20231130084017-bb00604d650c/go.mod h1:9iglf1GG4oNRJ39bZ5AZrjgAFD2RwQbXw6Qf7Cs47wo= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I= github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M= @@ -1236,8 +1238,10 @@ github.com/package-url/packageurl-go v0.1.3 h1:4juMED3hHiz0set3Vq3KeQ75KD1avthoX github.com/package-url/packageurl-go v0.1.3/go.mod h1:nKAWB8E6uk1MHqiS/lQb9pYBGH2+mdJ2PJc2s50dQY0= github.com/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw= github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= -github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= -github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI= @@ -2223,6 +2227,8 @@ k8s.io/client-go v0.30.0 h1:sB1AGGlhY/o7KCyCEQ0bPWzYDL0pwOZO4vAtTSh/gJQ= k8s.io/client-go v0.30.0/go.mod h1:g7li5O5256qe6TYdAMyX/otJqMhIiGgTapdLchhmOaY= k8s.io/component-base v0.30.0 h1:cj6bp38g0ainlfYtaOQuRELh5KSYjhKxM+io7AUIk4o= k8s.io/component-base v0.30.0/go.mod h1:V9x/0ePFNaKeKYA3bOvIbrNoluTSG+fSJKjLdjOoeXQ= +k8s.io/cri-api v0.27.1 h1:KWO+U8MfI9drXB/P4oU9VchaWYOlwDglJZVHWMpTT3Q= +k8s.io/cri-api v0.27.1/go.mod h1:+Ts/AVYbIo04S86XbTD73UPp/DkTiYxtsFeOFEu32L0= k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= diff --git a/internal/scheduler/scheduler.go b/internal/scheduler/scheduler.go index f12c20e..e0f9f93 100644 --- a/internal/scheduler/scheduler.go +++ b/internal/scheduler/scheduler.go @@ -75,7 +75,10 @@ func (s *BasicScheduler) Schedule(process Process) error { } // Add the process to the scheduler _, err := s.cron.AddFunc(process.GetCronExpr(), func() { - s.executeProcess(process) + err := s.executeProcess(process) + if err != nil { + log.Error().Err(err).Msgf("Error executing process %s", process.GetName()) + } }) if err != nil { return fmt.Errorf("error adding process to scheduler: %w", err) @@ -91,6 +94,8 @@ func (s *BasicScheduler) Start() error { } func (s *BasicScheduler) Stop() error { + s.mu.Lock() + defer s.mu.Unlock() s.stopped = true s.cron.Stop() return nil diff --git a/internal/state/artifact.go b/internal/state/artifact.go index 81ff6c8..2a80fcc 100644 --- a/internal/state/artifact.go +++ b/internal/state/artifact.go @@ -39,6 +39,10 @@ func NewArtifact(deleted bool, repository string, tags []string, digest, artifac } } +func (a *Artifact) GetLabels() []string { + return a.Labels +} + func (a *Artifact) GetRepository() string { return a.Repository } diff --git a/internal/utils/folder.go b/internal/utils/folder.go new file mode 100644 index 0000000..88b54bb --- /dev/null +++ b/internal/utils/folder.go @@ -0,0 +1,24 @@ +package utils + +import ( + "fmt" + "os" + "path/filepath" +) + +func CreateRuntimeDirectory(dir string) error { + cwd, err := os.Getwd() + if err != nil { + return fmt.Errorf("failed to get current working directory: %v", err) + } + runtimePath := filepath.Join(cwd, dir) + //check if the runtime directory exists + if _, err := os.Stat(runtimePath); os.IsNotExist(err) { + //create the runtime directory + err = os.MkdirAll(dir, 0755) + if err != nil { + return fmt.Errorf("failed to create the runtime directory: %v, please create a folder %v", err, dir) + } + } + return nil +} diff --git a/internal/utils/utils.go b/internal/utils/utils.go index 0b2ce7d..64aff3e 100644 --- a/internal/utils/utils.go +++ b/internal/utils/utils.go @@ -1,17 +1,22 @@ package utils import ( + "context" "errors" "fmt" "net" "net/url" "os" + "os/signal" "path/filepath" "strconv" "strings" + "syscall" "container-registry.com/harbor-satellite/internal/config" + "container-registry.com/harbor-satellite/logger" "container-registry.com/harbor-satellite/registry" + "github.com/spf13/cobra" ) // / ValidateRegistryAddress validates the registry address and port and returns the URL @@ -118,6 +123,17 @@ func FormatDuration(input string) (string, error) { return result, nil } +func SetupContext(context context.Context) (context.Context, context.CancelFunc) { + ctx, cancel := signal.NotifyContext(context, syscall.SIGTERM, syscall.SIGINT) + return ctx, cancel +} + +func SetupContextForCommand(cmd *cobra.Command) { + ctx := cmd.Context() + ctx = logger.AddLoggerToContext(ctx, config.GetLogLevel()) + cmd.SetContext(ctx) +} + // FormatRegistryURL formats the registry URL by trimming the "https://" or "http://" prefix if present func FormatRegistryURL(url string) string { // Trim the "https://" or "http://" prefix if present @@ -125,3 +141,37 @@ func FormatRegistryURL(url string) string { url = strings.TrimPrefix(url, "http://") return url } + +func ReadFile(path string, shouldPrint bool) ([]byte, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + if shouldPrint { + PrintData(string(data)) + } + return data, nil +} + +// PrintData prints the content of a file line by line +func PrintData(content string) { + lines := strings.Split(content, "\n") + fmt.Print("\n") + for i, line := range lines { + fmt.Printf("%5d | %s\n", i+1, line) + } +} + +// WriteFile takes the path and the data wand write the data to the file +func WriteFile(path string, data []byte) error { + file, err := os.Create(path) + if err != nil { + return fmt.Errorf("error creating file :%s", err) + } + defer file.Close() + _, err = file.Write(data) + if err != nil { + return fmt.Errorf("error while write to the file :%s", err) + } + return nil +} diff --git a/main.go b/main.go index 2bff96c..a06f6cf 100644 --- a/main.go +++ b/main.go @@ -1,113 +1,15 @@ package main import ( - "context" "fmt" "os" - "os/signal" - "syscall" - - "container-registry.com/harbor-satellite/internal/config" - "container-registry.com/harbor-satellite/internal/satellite" - "container-registry.com/harbor-satellite/internal/scheduler" - "container-registry.com/harbor-satellite/internal/server" - "container-registry.com/harbor-satellite/internal/utils" - "container-registry.com/harbor-satellite/logger" - "golang.org/x/sync/errgroup" - - "github.com/rs/zerolog" + "container-registry.com/harbor-satellite/cmd" ) func main() { - if err := run(); err != nil { + err := cmd.Execute() + if err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) os.Exit(1) } } - -func run() error { - // Initialize Config and Logger - if err := initConfig(); err != nil { - return err - } - - ctx, cancel := setupContext() - defer cancel() - - g, ctx := errgroup.WithContext(ctx) - ctx = logger.AddLoggerToContext(ctx, config.GetLogLevel()) - log := logger.FromContext(ctx) - - // Set up router and app - app := setupServerApp(ctx, log) - app.SetupRoutes() - app.SetupServer(g) - - // Handle registry setup - if err := handleRegistrySetup(g, log, cancel); err != nil { - return err - } - scheduler := scheduler.NewBasicScheduler(ctx) - ctx = context.WithValue(ctx, scheduler.GetSchedulerKey(), scheduler) - err := scheduler.Start() - if err != nil { - log.Error().Err(err).Msg("Error starting scheduler") - return err - } - - satelliteService := satellite.NewSatellite(ctx, scheduler.GetSchedulerKey()) - - g.Go(func() error { - return satelliteService.Run(ctx) - }) - - log.Info().Msg("Startup complete 🚀") - return g.Wait() -} - -func initConfig() error { - if err := config.InitConfig(); err != nil { - return fmt.Errorf("error initializing config: %w", err) - } - return nil -} - -func setupContext() (context.Context, context.CancelFunc) { - ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT) - return ctx, cancel -} - -func setupServerApp(ctx context.Context, log *zerolog.Logger) *server.App { - router := server.NewDefaultRouter("/api/v1") - router.Use(server.LoggingMiddleware) - - return server.NewApp( - router, - ctx, - log, - &server.MetricsRegistrar{}, - &server.DebugRegistrar{}, - &satellite.SatelliteRegistrar{}, - ) -} - -func handleRegistrySetup(g *errgroup.Group, log *zerolog.Logger, cancel context.CancelFunc) error { - if config.GetOwnRegistry() { - if err := utils.HandleOwnRegistry(); err != nil { - log.Error().Err(err).Msg("Error handling own registry") - return err - } - } else { - log.Info().Msg("Launching default registry") - g.Go(func() error { - if err := utils.LaunchDefaultZotRegistry(); err != nil { - log.Error().Err(err).Msg("Error launching default registry") - cancel() - return err - } - cancel() - return nil - }) - } - return nil -} diff --git a/registry/default_config.go b/registry/default_config.go new file mode 100644 index 0000000..ec5bc2e --- /dev/null +++ b/registry/default_config.go @@ -0,0 +1,55 @@ +package registry + +import ( + "encoding/json" + "fmt" + "io" + "os" + "strings" +) + +type DefaultZotConfig struct { + DistSpecVersion string `json:"distSpecVersion"` + Storage struct { + RootDirectory string `json:"rootDirectory"` + } `json:"storage"` + HTTP struct { + Address string `json:"address"` + Port string `json:"port"` + } `json:"http"` + Log struct { + Level string `json:"level"` + } `json:"log"` +} + +func (c *DefaultZotConfig) GetLocalRegistryURL() string { + address := c.HTTP.Address + if !strings.HasPrefix(address, "http://") && !strings.HasPrefix(address, "https://") { + address = "http://" + address + } + return fmt.Sprintf("%s:%s", address, c.HTTP.Port) +} + +// ReadConfig reads a JSON file from the specified path and unmarshals it into a Config struct. +func ReadConfig(filePath string) (*DefaultZotConfig, error) { + file, err := os.Open(filePath) + if err != nil { + return nil, fmt.Errorf("could not open file: %w", err) + } + defer file.Close() + + // Read the file contents + bytes, err := io.ReadAll(file) + if err != nil { + return nil, fmt.Errorf("could not read file: %w", err) + } + + // Unmarshal the JSON into a Config struct + var config DefaultZotConfig + err = json.Unmarshal(bytes, &config) + if err != nil { + return nil, fmt.Errorf("could not unmarshal JSON: %w", err) + } + + return &config, nil +} diff --git a/value/io.containerd.metadata.v1.bolt/meta.db b/value/io.containerd.metadata.v1.bolt/meta.db new file mode 100644 index 0000000..1884558 Binary files /dev/null and b/value/io.containerd.metadata.v1.bolt/meta.db differ