diff --git a/e2e/basic/observability_test.go b/e2e/basic/observability_test.go index 746f1d7..b78d9c8 100644 --- a/e2e/basic/observability_test.go +++ b/e2e/basic/observability_test.go @@ -18,11 +18,12 @@ const ( prometheusConfig = "/etc/prometheus/prometheus.yml" prometheusArgs = "--config.file=" + prometheusConfig - curlImage = "curlimages/curl:latest" - otlpPort = observability.DefaultOtelOtlpPort + curlImage = "curlimages/curl:latest" + telemetryPort = observability.DefaultTelemetryPort + otlpPort = observability.DefaultOtelOtlpPort - retryInterval = 1 * time.Second - retryTimeout = 10 * time.Second + retryInterval = 3 * time.Second + retryTimeout = 5 * time.Minute ) // TestObservabilityCollector is a test function that verifies the functionality of the otel collector setup @@ -58,7 +59,7 @@ global: scrape_configs: - job_name: 'otel-collector' static_configs: - - targets: ['otel-collector:%d']`, scrapeInterval, otlpPort) + - targets: ['localhost:%d']`, scrapeInterval, telemetryPort) s.Require().NoError(prometheus.Storage().AddFileBytes([]byte(prometheusConfigContent), prometheusConfig, "0:0")) s.Require().NoError(prometheus.Build().SetArgs(prometheusArgs)) @@ -68,9 +69,9 @@ scrape_configs: observabilitySidecar := observability.New() s.Require().NoError(observabilitySidecar.SetOtelEndpoint(4318)) - s.Require().NoError(observabilitySidecar.SetPrometheusEndpoint(otlpPort, fmt.Sprintf("knuu-%s", s.Knuu.Scope), scrapeInterval)) + s.Require().NoError(observabilitySidecar.SetPrometheusEndpoint(telemetryPort, fmt.Sprintf("knuu-%s", s.Knuu.Scope), scrapeInterval)) s.Require().NoError(observabilitySidecar.SetJaegerEndpoint(14250, 6831, 14268)) - s.Require().NoError(observabilitySidecar.SetOtlpExporter("prometheus:9090", "", "")) + s.Require().NoError(observabilitySidecar.SetOtlpExporter("http://prometheus:9090", "", "")) // Create and start a target pod and configure it to use the obsySidecar to push metrics target, err := s.Knuu.NewInstance(namePrefix + "-target") @@ -104,7 +105,11 @@ scrape_configs: return false } if resp.StatusCode != http.StatusOK { - s.T().Logf("Prometheus API returned status code: %d\tRetrying...", resp.StatusCode) + body, err := io.ReadAll(resp.Body) + if err != nil { + s.T().Logf("Failed to read response body: %v", err) + } + s.T().Logf("Prometheus API returned status code: %d\tRetrying...\nResponse: %s", resp.StatusCode, string(body)) return false } @@ -117,13 +122,14 @@ scrape_configs: return strings.Contains(string(body), "otel-collector") }, retryTimeout, retryInterval, "otel-collector data source not found in Prometheus") + } func (s *Suite) TestObservabilityCollectorWithLogging() { const ( - namePrefix = "observability" - targetStartCommand = "while true; do curl -X POST http://localhost:8888/v1/traces; sleep 2; done" + namePrefix = "observability" ) + targetStartCommand := fmt.Sprintf("while true; do curl -X POST http://localhost:%d/v1/traces; sleep 2; done", otlpPort) ctx := context.Background() // Setup obsySidecar collector diff --git a/e2e/suite.go b/e2e/suite.go index ab89b53..0665705 100644 --- a/e2e/suite.go +++ b/e2e/suite.go @@ -31,6 +31,8 @@ type Suite struct { cleanupMu sync.Mutex totalTests atomic.Int32 finishedTests int32 + + skipCleanup atomic.Bool // just for debugging } var ( @@ -57,12 +59,20 @@ func (s *Suite) TearDownTest() { func (s *Suite) cleanupSuite() { s.T().Logf("Cleaning up knuu...") + if s.skipCleanup.Load() { + s.T().Logf("* Cleanup skipped. Note: this is just for debugging purposes. Make sure to remove the SkipCleanup() call in the test before merging.") + return + } if err := s.Knuu.CleanUp(context.Background()); err != nil { s.T().Logf("Error cleaning up test suite: %v", err) } s.T().Logf("Knuu is cleaned up") } +func (s *Suite) SkipCleanup() { + s.skipCleanup.Store(true) +} + func (s *Suite) CreateNginxInstance(ctx context.Context, name string) *instance.Instance { ins, err := s.Knuu.NewInstance(name) s.Require().NoError(err) diff --git a/e2e/system/build_image_test.go b/e2e/system/build_image_test.go index 585fa56..9245512 100644 --- a/e2e/system/build_image_test.go +++ b/e2e/system/build_image_test.go @@ -19,7 +19,6 @@ func (s *Suite) TestBuildFromGit() { // Setup ctx := context.Background() - s.T().Log("Creating new instance") target, err := s.Knuu.NewInstance(namePrefix) s.Require().NoError(err, "Error creating new instance") @@ -61,7 +60,6 @@ func (s *Suite) TestBuildFromGitWithModifications() { // Setup ctx := context.Background() - s.T().Log("Creating new instance") target, err := s.Knuu.NewInstance(namePrefix) s.Require().NoError(err) @@ -79,7 +77,7 @@ func (s *Suite) TestBuildFromGitWithModifications() { expectedData = "Hello, world!" ) - err = target.Storage().AddFileBytes([]byte(expectedData), filePath, "root:root") + err = target.Storage().AddFileBytes([]byte(expectedData), filePath, "0:0") s.Require().NoError(err) s.Require().NoError(target.Build().Commit(ctx)) diff --git a/pkg/instance/build.go b/pkg/instance/build.go index 16714a5..47fdec0 100644 --- a/pkg/instance/build.go +++ b/pkg/instance/build.go @@ -15,11 +15,16 @@ import ( "github.com/celestiaorg/knuu/pkg/container" ) +const ( + baseInitImageName = "alpine:latest" +) + type build struct { instance *Instance imageName string - imagePullPolicy v1.PullPolicy + initImageName string builderFactory *container.BuilderFactory + imagePullPolicy v1.PullPolicy command []string args []string env map[string]string @@ -160,6 +165,56 @@ func (b *build) SetUser(user string) error { return nil } +// buildInitImage builds the init image for the instance +// This function can only be called in the state 'Preparing' +func (b *build) buildInitImage(ctx context.Context) error { + if !b.instance.IsState(StatePreparing) { + return ErrBuildingInitImageNotAllowed.WithParams(b.instance.state.String()) + } + + buildDir, err := b.getBuildDir() + if err != nil { + return ErrGettingBuildDir.Wrap(err) + } + + factory, err := container.NewBuilderFactory(container.BuilderFactoryOptions{ + ImageName: baseInitImageName, + BuildContext: buildDir, + ImageBuilder: b.instance.ImageBuilder, + Logger: b.instance.Logger, + }) + if err != nil { + return ErrCreatingBuilder.Wrap(err) + } + + mustBuild := false + for _, vol := range b.instance.storage.volumes { + for _, f := range vol.Files() { + // the files are copied to the build dir with the subfolder structure of dest + factory.AddToBuilder(f.Dest, f.Dest, f.Chown) + mustBuild = true + } + } + + if !mustBuild { + return nil + } + + imageHash, err := factory.GenerateImageHash() + if err != nil { + return ErrGeneratingImageHash.Wrap(err) + } + + // TODO: update this when the custom registry PR is merged (#593) + imageName, err := getImageRegistry(imageHash) + if err != nil { + return ErrGettingImageRegistry.Wrap(err) + } + + b.initImageName = imageName + return factory.PushBuilderImage(ctx, imageName) +} + func (b *build) SetNodeSelector(nodeSelector map[string]string) error { if !b.instance.IsInState(StatePreparing, StateCommitted) { return ErrSettingNodeSelectorNotAllowed.WithParams(b.instance.state.String()) @@ -176,6 +231,12 @@ func (b *build) Commit(ctx context.Context) error { return ErrCommittingNotAllowed.WithParams(b.instance.state.String()) } + if len(b.instance.storage.volumes) > 0 { + if err := b.buildInitImage(ctx); err != nil { + return err + } + } + if !b.builderFactory.Changed() { b.imageName = b.builderFactory.ImageNameFrom() b.instance.Logger.WithFields(logrus.Fields{ @@ -254,7 +315,8 @@ func (b *build) getBuildDir() (string, error) { if err != nil { return "", err } - return filepath.Join(tmpDir, b.instance.name), nil + b.buildDir = filepath.Join(tmpDir, b.instance.name) + return b.buildDir, nil } // addFileToBuilder adds a file to the builder diff --git a/pkg/instance/errors.go b/pkg/instance/errors.go index 7dd6d6a..efd2647 100644 --- a/pkg/instance/errors.go +++ b/pkg/instance/errors.go @@ -232,4 +232,6 @@ var ( ErrFailedToGetFileSize = errors.New("FailedToGetFileSize", "failed to get file size") ErrFileTooLargeCommitted = errors.New("FileTooLargeCommitted", "file '%s' is too large (max 1MiB) to add after instance is committed") ErrTotalFilesSizeTooLarge = errors.New("TotalFilesSizeTooLarge", "total files size is too large (max 1MiB)") + ErrFileAlreadyExistsInTheVolumePath = errors.New("FileAlreadyExistsInTheVolumePath", "file %s already exists in the volume path %s") + ErrBuildingInitImageNotAllowed = errors.New("BuildingInitImageNotAllowed", "building init image is only allowed in state 'Preparing'. Current state is '%s'") ) diff --git a/pkg/instance/execution.go b/pkg/instance/execution.go index 622c5fb..0af1131 100644 --- a/pkg/instance/execution.go +++ b/pkg/instance/execution.go @@ -386,6 +386,7 @@ func (e *execution) prepareReplicaSetConfig() k8s.ReplicaSetConfig { SecurityContext: e.instance.security.prepareSecurityContext(), TCPPorts: e.instance.network.portsTCP, UDPPorts: e.instance.network.portsUDP, + InitImageName: e.instance.build.initImageName, } sidecarConfigs := make([]k8s.ContainerConfig, 0) diff --git a/pkg/instance/storage.go b/pkg/instance/storage.go index b2343ec..0cb5c30 100644 --- a/pkg/instance/storage.go +++ b/pkg/instance/storage.go @@ -49,32 +49,27 @@ func (s *storage) AddFile(src string, dest string, chown string) error { return err } - switch s.instance.state { - case StatePreparing: - s.instance.build.addFileToBuilder(src, dest, chown) - return nil - case StateCommitted, StateStopped: - srcInfo, err := os.Stat(src) + if s.instance.IsState(StatePreparing) { + volume, err := s.findMatchingVolume(dest) if err != nil { - return ErrFailedToGetFileSize.Wrap(err) + return err } - if srcInfo.Size() > maxTotalFilesBytes { - return ErrFileTooLargeCommitted.WithParams(src) + if volume != nil { + return volume.AddFile(s.instance.K8sClient.NewFile(src, dest, chown, "")) } - return s.addFileToInstance(buildDirPath, dest, chown) + + s.instance.build.addFileToBuilder(src, dest, chown) + return nil } - buildDir, err := s.instance.build.getBuildDir() + srcInfo, err := os.Stat(src) if err != nil { - return ErrGettingBuildDir.Wrap(err) + return ErrFailedToGetFileSize.Wrap(err) } - s.instance.Logger.WithFields(logrus.Fields{ - "file": dest, - "instance": s.instance.name, - "state": s.instance.state, - "build_dir": buildDir, - }).Debug("added file") - return nil + if srcInfo.Size() > maxTotalFilesBytes { + return ErrFileTooLargeCommitted.WithParams(src) + } + return s.addFileToInstance(buildDirPath, dest, chown) } // AddFolder adds a folder to the instance @@ -168,14 +163,6 @@ func (s *storage) AddFileBytes(bytes []byte, dest string, chown string) error { // The owner of the volume is set to 0, if you want to set a custom owner use AddVolumeWithOwner // This function can only be called in the states 'Preparing', 'Committed' and 'Stopped' func (s *storage) AddVolume(path string, size resource.Quantity) error { - // temporary feat, we will remove it once we can add multiple volumes - if len(s.volumes) > 0 { - s.instance.Logger.WithFields(logrus.Fields{ - "instance": s.instance.name, - "volumes": len(s.volumes), - }).Debug("maximum volumes exceeded") - return ErrMaximumVolumesExceeded.WithParams(s.instance.name) - } return s.AddVolumeWithOwner(path, size, 0) } @@ -193,6 +180,17 @@ func (s *storage) AddVolumeWithOwner(path string, size resource.Quantity, owner }).Debug("maximum volumes exceeded") return ErrMaximumVolumesExceeded.WithParams(s.instance.name) } + + // When already a file exists in the volume path, then we cannot add a volume + // b/c the volume must be added first to avoid user error + file, err := s.findMatchingFile(path) + if err != nil { + return err + } + if file != nil { + return ErrFileAlreadyExistsInTheVolumePath.WithParams(file.Dest, path) + } + volume := s.instance.K8sClient.NewVolume(path, size, owner) s.volumes = append(s.volumes, volume) s.instance.Logger.WithFields(logrus.Fields{ @@ -384,9 +382,10 @@ func (s *storage) destroyVolume(ctx context.Context) error { // deployFiles deploys the files for the instance func (s *storage) deployFiles(ctx context.Context) error { - data := map[string]string{} + // key: dirPath, value: (configmap data) map[string]string{key: fileName, value: fileContent} + dirs := map[string]map[string]string{} - for i, file := range s.files { + for _, file := range s.files { // read out file content and assign to variable srcFile, err := os.Open(file.Source) if err != nil { @@ -401,20 +400,26 @@ func (s *storage) deployFiles(ctx context.Context) error { var ( fileContent = string(fileContentBytes) - keyName = fmt.Sprintf("%d", i) + keyName = filepath.Base(file.Dest) ) - data[keyName] = fileContent + if _, ok := dirs[filepath.Dir(file.Dest)]; !ok { + dirs[filepath.Dir(file.Dest)] = make(map[string]string) + } + dirs[filepath.Dir(file.Dest)][keyName] = fileContent } - // If the configmap already exists, we update it - // This ensures long-running tests and image upgrade tests function correctly. - _, err := s.instance.K8sClient.CreateOrUpdateConfigMap(ctx, s.instance.name, s.instance.execution.Labels(), data) - if err != nil { - return ErrFailedToCreateConfigMap.Wrap(err) - } + for dirPath, data := range dirs { + // If the configmap already exists, we update it + // This ensures long-running tests and image upgrade tests function correctly. + cmName := k8s.PrepareConfigMapName(s.instance.name, dirPath) + _, err := s.instance.K8sClient.CreateOrUpdateConfigMap(ctx, cmName, s.instance.execution.Labels(), data) + if err != nil { + return ErrFailedToCreateConfigMap.Wrap(err) + } - s.instance.Logger.WithField("configmap", s.instance.name).Debug("deployed configmap") + s.instance.Logger.WithField("configmap", s.instance.name).Debug("deployed configmap") + } return nil } @@ -497,3 +502,41 @@ func (s *storage) clone() *storage { files: filesCopy, } } + +func (s *storage) findMatchingVolume(filePath string) (*k8s.Volume, error) { + for _, v := range s.volumes { + ok, err := isSubpath(v.Path, filePath) + if err != nil { + return nil, err + } + if ok { + return v, nil + } + } + return nil, nil // no matching volume found +} + +func (s *storage) findMatchingFile(volumePath string) (*k8s.File, error) { + for _, f := range s.files { + ok, err := isSubpath(volumePath, f.Dest) + if err != nil { + return nil, err + } + if ok { + return f, nil + } + } + return nil, nil // no matching file found +} + +func isSubpath(base, target string) (bool, error) { + base = filepath.Clean(base) + target = filepath.Clean(target) + + rel, err := filepath.Rel(base, target) + if err != nil { + return false, err + } + + return !strings.HasPrefix(rel, ".."), nil +} diff --git a/pkg/k8s/configmap.go b/pkg/k8s/configmap.go index b4bd75f..eea90c7 100644 --- a/pkg/k8s/configmap.go +++ b/pkg/k8s/configmap.go @@ -3,10 +3,18 @@ package k8s import ( "context" "errors" + "fmt" v1 "k8s.io/api/core/v1" apierrs "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/validation" + + "github.com/celestiaorg/knuu/pkg/names" +) + +const ( + configMapNameHashSuffixLength = 8 ) func (c *Client) GetConfigMap(ctx context.Context, name string) (*v1.ConfigMap, error) { @@ -124,3 +132,12 @@ func prepareConfigMap( Data: data, } } + +func PrepareConfigMapName(instanceName, dirPath string) string { + maxInstanceNameLength := validation.DNS1123SubdomainMaxLength - configMapNameHashSuffixLength - 1 + if len(instanceName) > maxInstanceNameLength { + instanceName = instanceName[:maxInstanceNameLength] + } + hash := names.HashWithLength(dirPath, configMapNameHashSuffixLength) + return fmt.Sprintf("%s-%s", instanceName, hash) +} diff --git a/pkg/k8s/errors.go b/pkg/k8s/errors.go index d79833a..d9e7dcf 100644 --- a/pkg/k8s/errors.go +++ b/pkg/k8s/errors.go @@ -147,4 +147,5 @@ var ( ErrNoPortsFoundForService = errors.New("NoPortsFoundForService", "no ports found for service %s") ErrNoValidNodeIPFound = errors.New("NoValidNodeIPFound", "no valid node IP found for service %s") ErrInvalidClusterDomain = errors.New("InvalidClusterDomain", "invalid cluster domain `%s`") + ErrDestNotSubpath = errors.New("DestNotSubpath", "destination %s is not a subpath of volume path %s") ) diff --git a/pkg/k8s/pod.go b/pkg/k8s/pod.go index 035923a..4884b65 100644 --- a/pkg/k8s/pod.go +++ b/pkg/k8s/pod.go @@ -29,13 +29,13 @@ const ( podFilesConfigmapNameSuffix = "-config" initContainerNameSuffix = "-init" - initContainerImage = "nicolaka/netshoot" defaultContainerUser = 0 ) type ContainerConfig struct { Name string // Name to assign to the Container Image string // Name of the container image to use for the container + InitImageName string // InitImageName to use for the Pod ImagePullPolicy v1.PullPolicy // Image pull policy for the container Command []string // Command to run in the container Args []string // Arguments to pass to the command in the container @@ -65,19 +65,6 @@ type PodConfig struct { NodeSelector map[string]string // NodeSelector to apply to the Pod } -type Volume struct { - Path string - Size resource.Quantity - Owner int64 -} - -type File struct { - Source string - Dest string - Chown string - Permission string -} - // DeployPod creates a new pod in the namespace that k8s client is initiate with if it doesn't already exist. func (c *Client) DeployPod(ctx context.Context, podConfig PodConfig, init bool) (*v1.Pod, error) { if c.terminated { @@ -96,23 +83,6 @@ func (c *Client) DeployPod(ctx context.Context, podConfig PodConfig, init bool) return createdPod, nil } -func (c *Client) NewVolume(path string, size resource.Quantity, owner int64) *Volume { - return &Volume{ - Path: path, - Size: size, - Owner: owner, - } -} - -func (c *Client) NewFile(source, dest, chown, permission string) *File { - return &File{ - Source: source, - Dest: dest, - Chown: chown, - Permission: permission, - } -} - func (c *Client) ReplacePodWithGracePeriod(ctx context.Context, podConfig PodConfig, gracePeriod *int64) (*v1.Pod, error) { c.logger.WithField("name", podConfig.Name).Debug("replacing pod") @@ -370,10 +340,10 @@ func buildEnv(envMap map[string]string) []v1.EnvVar { // buildPodVolumes generates a volume configuration for a pod based on the given name. // If the volumes amount is zero, returns an empty slice. -func buildPodVolumes(name string, volumesAmount, filesAmount int) []v1.Volume { +func buildPodVolumes(name string, volumes []*Volume, files []*File) []v1.Volume { var podVolumes []v1.Volume - if volumesAmount != 0 { + if len(volumes) != 0 { podVolume := v1.Volume{ Name: name, VolumeSource: v1.VolumeSource{ @@ -386,29 +356,24 @@ func buildPodVolumes(name string, volumesAmount, filesAmount int) []v1.Volume { podVolumes = append(podVolumes, podVolume) } - if volumesAmount == 0 && filesAmount != 0 { - podVolume := v1.Volume{ - Name: name, - VolumeSource: v1.VolumeSource{ - EmptyDir: &v1.EmptyDirVolumeSource{}, - }, - } - podVolumes = append(podVolumes, podVolume) + if len(files) == 0 { + return podVolumes } - if filesAmount != 0 { + uniqueDirs := uniqueDirs(files) + for dir := range uniqueDirs { + cmName := PrepareConfigMapName(name, dir) podFiles := v1.Volume{ - Name: name + podFilesConfigmapNameSuffix, + Name: cmName, VolumeSource: v1.VolumeSource{ ConfigMap: &v1.ConfigMapVolumeSource{ LocalObjectReference: v1.LocalObjectReference{ - Name: name, + Name: cmName, }, - DefaultMode: ptr.To[int32](0600), + DefaultMode: ptr.To[int32](0644), }, }, } - podVolumes = append(podVolumes, podFiles) } @@ -419,33 +384,45 @@ func buildPodVolumes(name string, volumesAmount, filesAmount int) []v1.Volume { func buildContainerVolumes(name string, volumes []*Volume, files []*File) []v1.VolumeMount { var containerVolumes []v1.VolumeMount for _, volume := range volumes { - containerVolumes = append( - containerVolumes, - v1.VolumeMount{ - Name: name, - MountPath: volume.Path, - SubPath: strings.TrimLeft(volume.Path, "/"), - }, - ) + containerVolumes = append(containerVolumes, v1.VolumeMount{ + Name: name, + MountPath: volume.Path, + }) } - if len(volumes) == 0 && len(files) != 0 { - uniquePaths := make(map[string]bool) - for _, file := range files { - uniquePaths[filepath.Dir(file.Dest)] = true - } - for path := range uniquePaths { + if len(files) == 0 { + return containerVolumes + } + + mountedDirs := make(map[string]bool) + for _, file := range files { + dir := filepath.Dir(file.Dest) + cmName := PrepareConfigMapName(name, dir) + + // Since k8s is not allowed to mount a configmap to a critical dir (throws readonly file system error), + // we need to mount the configmap to the file + if isCriticalDir(dir) { containerVolumes = append(containerVolumes, v1.VolumeMount{ - Name: name, - MountPath: path, - SubPath: strings.TrimPrefix(path, "/"), + Name: cmName, + MountPath: file.Dest, + SubPath: filepath.Base(file.Dest), }) + continue } - } - var containerFiles []v1.VolumeMount + // if the dir is not in a critical dir, we need to mount the configmap to the dir + if _, processed := mountedDirs[dir]; processed { + continue + } - return append(containerVolumes, containerFiles...) + containerVolumes = append(containerVolumes, v1.VolumeMount{ + Name: cmName, + MountPath: dir, + }) + mountedDirs[dir] = true + } + + return containerVolumes } // buildInitContainerVolumes generates a volume mount configuration for an init container based on the given name and volumes. @@ -471,18 +448,18 @@ func buildInitContainerVolumes(name string, volumes []*Volume, files []*File) [] for path := range uniquePaths { containerVolumes = append(containerVolumes, v1.VolumeMount{ Name: name, - MountPath: knuuPath + path, - SubPath: strings.TrimPrefix(path, "/"), + MountPath: filepath.Join(knuuPath, path), + SubPath: filepath.Base(path), }) } } var containerFiles []v1.VolumeMount - for n, file := range files { + for _, file := range files { containerFiles = append(containerFiles, v1.VolumeMount{ Name: name + podFilesConfigmapNameSuffix, MountPath: file.Dest, - SubPath: fmt.Sprintf("%d", n), + SubPath: filepath.Base(file.Dest), }) } @@ -505,7 +482,7 @@ func (c *Client) buildInitContainerCommand(volumes []*Volume, files []*File) []s folder := filepath.Dir(file.Dest) if _, processed := dirsProcessed[folder]; !processed { var ( - knuuFolder = fmt.Sprintf("%s%s", knuuPath, folder) + knuuFolder = filepath.Join(knuuPath, folder) parentDirCmd = fmt.Sprintf("mkdir -p %s && ", knuuFolder) ) cmds = append(cmds, parentDirCmd) @@ -524,8 +501,9 @@ func (c *Client) buildInitContainerCommand(volumes []*Volume, files []*File) []s } // for each volume, copy the contents of the volume to the knuu volume + // TODO: this code works only for one volume, need to fix it for _, volume := range volumes { - knuuVolumePath := fmt.Sprintf("%s%s", knuuPath, volume.Path) + knuuVolumePath := knuuPath // volume is mounted to the same path so no need to join the path here cmd := fmt.Sprintf("if [ -d %s ] && [ \"$(ls -A %s)\" ]; then mkdir -p %s && cp -r %s/* %s && chown -R %d:%d %s", volume.Path, volume.Path, knuuVolumePath, volume.Path, knuuVolumePath, volume.Owner, volume.Owner, knuuVolumePath) @@ -596,14 +574,16 @@ func prepareContainer(config ContainerConfig) v1.Container { // prepareInitContainers creates a slice of v1.Container as init containers. func (c *Client) prepareInitContainers(config ContainerConfig, init bool) []v1.Container { - if !init || (len(config.Volumes) == 0 && len(config.Files) == 0) { + if !init || + (len(config.Volumes) == 0 && len(config.Files) == 0) || + config.InitImageName == "" { return nil } return []v1.Container{ { Name: config.Name + initContainerNameSuffix, - Image: initContainerImage, + Image: config.InitImageName, SecurityContext: &v1.SecurityContext{ RunAsUser: ptr.To[int64](defaultContainerUser), }, @@ -615,7 +595,7 @@ func (c *Client) prepareInitContainers(config ContainerConfig, init bool) []v1.C // preparePodVolumes prepares pod volumes func preparePodVolumes(config ContainerConfig) []v1.Volume { - return buildPodVolumes(config.Name, len(config.Volumes), len(config.Files)) + return buildPodVolumes(config.Name, config.Volumes, config.Files) } func (c *Client) preparePodSpec(spec PodConfig, init bool) v1.PodSpec { @@ -658,3 +638,30 @@ func (c *Client) preparePod(spec PodConfig, init bool) *v1.Pod { }).Debug("prepared pod") return pod } + +func uniqueDirs(files []*File) map[string]bool { + uniqueDirs := make(map[string]bool) + for _, file := range files { + uniqueDirs[filepath.Dir(file.Dest)] = true + } + return uniqueDirs +} + +func isCriticalDir(dir string) bool { + criticalDirs := map[string]bool{ + "/etc": true, + "/bin": true, + "/sbin": true, + "/lib": true, + "/lib64": true, + "/dev": true, + "/proc": true, + "/sys": true, + "/run": true, + "/boot": true, + "/usr": true, + "/var": true, + "/root": true, + } + return criticalDirs[dir] +} diff --git a/pkg/k8s/volume.go b/pkg/k8s/volume.go new file mode 100644 index 0000000..c2c57a7 --- /dev/null +++ b/pkg/k8s/volume.go @@ -0,0 +1,72 @@ +package k8s + +import ( + "path/filepath" + "strings" + + "k8s.io/apimachinery/pkg/api/resource" +) + +type Volume struct { + Path string + Size resource.Quantity + Owner int64 + files []*File +} + +type File struct { + Source string + Dest string + Chown string + Permission string +} + +func (c *Client) NewFile(source, dest, chown, permission string) *File { + return &File{ + Source: source, + Dest: dest, + Chown: chown, + Permission: permission, + } +} + +func (c *Client) NewVolume(path string, size resource.Quantity, owner int64) *Volume { + return &Volume{ + Path: path, + Size: size, + Owner: owner, + } +} + +func (v *Volume) AddFile(f *File) error { + if err := validateFile(f); err != nil { + return err + } + + ok, err := v.isSubpath(f.Dest) + if err != nil { + return err + } + if !ok { + return ErrDestNotSubpath.WithParams(f.Dest, v.Path) + } + + v.files = append(v.files, f) + return nil +} + +func (v *Volume) Files() []*File { + return v.files +} + +func (v *Volume) isSubpath(target string) (bool, error) { + base := filepath.Clean(v.Path) + target = filepath.Clean(target) + + rel, err := filepath.Rel(base, target) + if err != nil { + return false, err + } + + return !strings.HasPrefix(rel, ".."), nil +} diff --git a/pkg/names/names.go b/pkg/names/names.go index 7b63f7d..f13afb5 100644 --- a/pkg/names/names.go +++ b/pkg/names/names.go @@ -1,6 +1,8 @@ package names import ( + "crypto/sha1" + "encoding/hex" "fmt" "github.com/google/uuid" @@ -14,3 +16,8 @@ func NewRandomK8(prefix string) (string, error) { } return fmt.Sprintf("%s-%s", prefix, uuid.String()[:8]), nil } + +func HashWithLength(input string, length int) string { + hash := sha1.Sum([]byte(input)) + return hex.EncodeToString(hash[:])[:length] +} diff --git a/pkg/sidecars/observability/obsy.go b/pkg/sidecars/observability/obsy.go index f96f4dc..dd74697 100644 --- a/pkg/sidecars/observability/obsy.go +++ b/pkg/sidecars/observability/obsy.go @@ -12,7 +12,8 @@ import ( ) const ( - DefaultOtelOtlpPort = 8888 + DefaultOtelOtlpPort = 4318 + DefaultTelemetryPort = 8888 DefaultOtelMetricsPort = 9090 DefaultImage = "otel/opentelemetry-collector-contrib:%s" DefaultOtelCollectorVersion = "0.83.0" @@ -101,7 +102,7 @@ func (o *Obsy) Initialize(ctx context.Context, namePrefix string, sysDeps *syste if err != nil { return ErrSettingOtelAgentImage.Wrap(err) } - if err := o.instance.Network().AddPortTCP(DefaultOtelOtlpPort); err != nil { + if err := o.instance.Network().AddPortTCP(DefaultTelemetryPort); err != nil { return ErrAddingOtelAgentPort.Wrap(err) } if err := o.instance.Network().AddPortTCP(DefaultOtelMetricsPort); err != nil {