Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ability for project clone to 'bootstrap' devworkspaces #1193

Merged
merged 2 commits into from
Nov 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion controllers/workspace/devworkspace_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@ func (r *DevWorkspaceReconciler) Reconcile(ctx context.Context, req ctrl.Request
// Add init container to clone projects
projectCloneOptions := projects.Options{
Image: workspace.Config.Workspace.ProjectCloneConfig.Image,
Env: workspace.Config.Workspace.ProjectCloneConfig.Env,
Env: env.GetEnvironmentVariablesForProjectClone(workspace),
Resources: workspace.Config.Workspace.ProjectCloneConfig.Resources,
}
if workspace.Config.Workspace.ProjectCloneConfig.ImagePullPolicy != "" {
Expand Down
6 changes: 6 additions & 0 deletions pkg/constants/attributes.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,4 +145,10 @@ const (
// StarterProjectAttribute is an attribute applied to the top-level attributes in a DevWorkspace to specify which
// starterProject in the workspace should be cloned.
StarterProjectAttribute = "controller.devfile.io/use-starter-project"

// BootstrapDevWorkspaceAttribute is an attribute applied to the top-level attributes in a DevWorkspace to configure
// the project-clone container to "bootstrap" the DevWorkspace from a devfile.yaml or .devfile.yaml file at the root
// of a cloned project. If the bootstrap process is successful, project-clone will automatically remove this attribute
// from the DevWorkspace
BootstrapDevWorkspaceAttribute = "controller.devfile.io/bootstrap-devworkspace"
)
27 changes: 19 additions & 8 deletions pkg/library/env/workspaceenv.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ import (

dw "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
"github.com/devfile/devworkspace-operator/apis/controller/v1alpha1"
devfileConstants "github.com/devfile/devworkspace-operator/pkg/library/constants"
corev1 "k8s.io/api/core/v1"
v1 "k8s.io/api/core/v1"

"github.com/devfile/devworkspace-operator/pkg/common"
"github.com/devfile/devworkspace-operator/pkg/constants"
Expand All @@ -47,6 +47,17 @@ func AddCommonEnvironmentVariables(podAdditions *v1alpha1.PodAdditions, clusterD
return nil
}

func GetEnvironmentVariablesForProjectClone(workspace *common.DevWorkspaceWithConfig) []corev1.EnvVar {
var cloneEnv []corev1.EnvVar
cloneEnv = append(cloneEnv, workspace.Config.Workspace.ProjectCloneConfig.Env...)
cloneEnv = append(cloneEnv, commonEnvironmentVariables(workspace)...)
cloneEnv = append(cloneEnv, corev1.EnvVar{
Name: devfileConstants.ProjectsRootEnvVar,
Value: constants.DefaultProjectsSourcesRoot,
})
return cloneEnv
}

func commonEnvironmentVariables(workspaceWithConfig *common.DevWorkspaceWithConfig) []corev1.EnvVar {
envvars := []corev1.EnvVar{
{
Expand Down Expand Up @@ -88,20 +99,20 @@ func GetProxyEnvVars(proxyConfig *v1alpha1.Proxy) []corev1.EnvVar {

// Proxy env vars are defined by consensus rather than standard; most tools use the lower-snake-case version
// but some may only look at the upper-snake-case version, so we add both.
var env []v1.EnvVar
var env []corev1.EnvVar
if proxyConfig.HttpProxy != nil {
env = append(env, v1.EnvVar{Name: "http_proxy", Value: *proxyConfig.HttpProxy})
env = append(env, v1.EnvVar{Name: "HTTP_PROXY", Value: *proxyConfig.HttpProxy})
env = append(env, corev1.EnvVar{Name: "http_proxy", Value: *proxyConfig.HttpProxy})
env = append(env, corev1.EnvVar{Name: "HTTP_PROXY", Value: *proxyConfig.HttpProxy})
}
if proxyConfig.HttpsProxy != nil {
env = append(env, v1.EnvVar{Name: "https_proxy", Value: *proxyConfig.HttpsProxy})
env = append(env, v1.EnvVar{Name: "HTTPS_PROXY", Value: *proxyConfig.HttpsProxy})
env = append(env, corev1.EnvVar{Name: "https_proxy", Value: *proxyConfig.HttpsProxy})
env = append(env, corev1.EnvVar{Name: "HTTPS_PROXY", Value: *proxyConfig.HttpsProxy})
}
if proxyConfig.NoProxy != nil {
// Adding 'KUBERNETES_SERVICE_HOST' env var to the 'no_proxy / NO_PROXY' list. Hot Fix for https://issues.redhat.com/browse/CRW-2820
kubernetesServiceHost := os.Getenv("KUBERNETES_SERVICE_HOST")
env = append(env, v1.EnvVar{Name: "no_proxy", Value: *proxyConfig.NoProxy + "," + kubernetesServiceHost})
env = append(env, v1.EnvVar{Name: "NO_PROXY", Value: *proxyConfig.NoProxy + "," + kubernetesServiceHost})
env = append(env, corev1.EnvVar{Name: "no_proxy", Value: *proxyConfig.NoProxy + "," + kubernetesServiceHost})
env = append(env, corev1.EnvVar{Name: "NO_PROXY", Value: *proxyConfig.NoProxy + "," + kubernetesServiceHost})
}

return env
Expand Down
12 changes: 1 addition & 11 deletions pkg/library/projects/clone.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import (
dw "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
controllerv1alpha1 "github.com/devfile/devworkspace-operator/apis/controller/v1alpha1"
devfileConstants "github.com/devfile/devworkspace-operator/pkg/library/constants"
"github.com/devfile/devworkspace-operator/pkg/library/env"
dwResources "github.com/devfile/devworkspace-operator/pkg/library/resources"
corev1 "k8s.io/api/core/v1"

Expand Down Expand Up @@ -68,15 +67,6 @@ func GetProjectCloneInitContainer(workspace *dw.DevWorkspaceTemplateSpec, option
return nil, nil
}

cloneEnv := []corev1.EnvVar{
{
Name: devfileConstants.ProjectsRootEnvVar,
Value: constants.DefaultProjectsSourcesRoot,
},
}
cloneEnv = append(cloneEnv, env.GetProxyEnvVars(proxyConfig)...)
cloneEnv = append(cloneEnv, options.Env...)

resources := dwResources.FilterResources(options.Resources)
if err := dwResources.ValidateResources(resources); err != nil {
return nil, fmt.Errorf("invalid resources for project clone container: %w", err)
Expand All @@ -85,7 +75,7 @@ func GetProjectCloneInitContainer(workspace *dw.DevWorkspaceTemplateSpec, option
return &corev1.Container{
Name: projectClonerContainerName,
Image: cloneImage,
Env: cloneEnv,
Env: options.Env,
Resources: *resources,
VolumeMounts: []corev1.VolumeMount{
{
Expand Down
123 changes: 123 additions & 0 deletions project-clone/internal/bootstrap/bootstrap.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// Copyright (c) 2019-2023 Red Hat, Inc.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package bootstrap

import (
"context"
"fmt"
"log"

dw "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
"github.com/devfile/api/v2/pkg/attributes"
"github.com/devfile/devworkspace-operator/pkg/constants"
)

var devfileNames = []string{"devfile.yaml", ".devfile.yaml"}

func NeedsBootstrap(dw *dw.DevWorkspaceTemplateSpec) (bool, error) {
if dw.Attributes == nil {
return false, nil
}
if !dw.Attributes.Exists(constants.BootstrapDevWorkspaceAttribute) {
return false, nil
}
var attrErr error
needBootstrap := dw.Attributes.GetBoolean(constants.BootstrapDevWorkspaceAttribute, &attrErr)
if attrErr != nil {
return false, attrErr
}
return needBootstrap, nil
}

func BootstrapWorkspace(flattenedWorkspace *dw.DevWorkspaceTemplateSpec) error {
devfile, projectName, err := getBootstrapDevfile(flattenedWorkspace.Projects)
if err != nil {
return err
}
log.Printf("Updating current DevWorkspace with content from devfile found in project %s", projectName)

kubeclient, err := setupKubeClient()
if err != nil {
return err
}

workspaceNN, err := getWorkspaceNamespacedName()
if err != nil {
return err
}

clusterWorkspace := &dw.DevWorkspace{}
if err := kubeclient.Get(context.Background(), workspaceNN, clusterWorkspace); err != nil {
return fmt.Errorf("failed to read DevWorkspace from cluster: %w", err)
}

updatedWorkspace := updateWorkspaceFromDevfile(clusterWorkspace, devfile)

if err := kubeclient.Update(context.Background(), updatedWorkspace); err != nil {
return fmt.Errorf("failed to update DevWorkspace on cluster: %w", err)
}

log.Printf("Successfully updated DevWorkspace. Workspace may restart")

return nil
}

func updateWorkspaceFromDevfile(workspace *dw.DevWorkspace, devfile *dw.DevWorkspaceTemplateSpec) *dw.DevWorkspace {
updated := workspace.DeepCopy()

// Use devfile contents for this DevWorkspace instead of whatever is there
updated.Spec.Template = *devfile

// Add attributes from original DevWorkspace, since it's assumed they're more relevant than the devfile's attributes
// This will override any attributes present in both the devfile and DevWorkspace with the latter's value
if updated.Spec.Template.Attributes == nil {
updated.Spec.Template.Attributes = attributes.Attributes{}
}
for key, value := range workspace.Spec.Template.Attributes {
updated.Spec.Template.Attributes[key] = value
}

// Merge projects; we want the DevWorkspace's projects to not be dropped from the workspace, but also want to add any projects
// present in the devfile. We also want workspace projects first in this list, since this is the order they're bootstrapped from
updated.Spec.Template.Projects = mergeProjects(workspace.Spec.Template.Projects, devfile.Projects)

// Remove bootstrap attribute to avoid unnecessarily doing this process in the future
delete(updated.Spec.Template.Attributes, constants.BootstrapDevWorkspaceAttribute)

return updated
}

func mergeProjects(workspaceProjects, devfileProjects []dw.Project) []dw.Project {
var allProjects []dw.Project

// Bookkeeping structs to avoid adding identical projects. We want to avoid an issue where DevWorkspace and devfile
// contain the same project; adding both to the workspace will cause the workspace to be invalid.
// An additional improvement in the future would be to avoid adding two very similar projects (e.g. identical projects
// with different names)
projectNames := map[string]bool{}

for _, project := range workspaceProjects {
projectNames[project.Name] = true
allProjects = append(allProjects, project)
}

for _, project := range devfileProjects {
if !projectNames[project.Name] {
projectNames[project.Name] = true
allProjects = append(allProjects, project)
}
}

return allProjects
}
61 changes: 61 additions & 0 deletions project-clone/internal/bootstrap/cluster.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Copyright (c) 2019-2023 Red Hat, Inc.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package bootstrap

import (
"fmt"
"os"

dw "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
"github.com/devfile/devworkspace-operator/pkg/constants"
k8sruntime "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/client"
)

func setupKubeClient() (client.Client, error) {
scheme := k8sruntime.NewScheme()

if err := dw.AddToScheme(scheme); err != nil {
return nil, fmt.Errorf("failed to set up Kubernetes client: %w", err)
}

cfg, err := rest.InClusterConfig()
if err != nil {
return nil, fmt.Errorf("failed to read in-cluster Kubernetes configuration: %w", err)
}

kubeClient, err := client.New(cfg, client.Options{Scheme: scheme})
if err != nil {
return nil, fmt.Errorf("failed to create Kubernetes client: %w", err)
}

return kubeClient, nil
}

func getWorkspaceNamespacedName() (types.NamespacedName, error) {
name := os.Getenv(constants.DevWorkspaceName)
namespace := os.Getenv(constants.DevWorkspaceNamespace)

namespacedName := types.NamespacedName{
Name: name,
Namespace: namespace,
}

if name == "" || namespace == "" {
return namespacedName, fmt.Errorf("could not get workspace name or namespace from environment variables")
}
return namespacedName, nil
}
57 changes: 57 additions & 0 deletions project-clone/internal/bootstrap/util.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright (c) 2019-2023 Red Hat, Inc.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package bootstrap

import (
"fmt"
"os"
"path"

dw "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
"github.com/devfile/devworkspace-operator/pkg/library/projects"
"github.com/devfile/devworkspace-operator/project-clone/internal"
"sigs.k8s.io/yaml"
)

func getBootstrapDevfile(projects []dw.Project) (devfile *dw.DevWorkspaceTemplateSpec, projectName string, err error) {
var devfileBytes []byte
var devfileProject string
for _, project := range projects {
bytes, err := getDevfileFromProject(project)
if err == nil && len(bytes) > 0 {
devfileBytes = bytes
devfileProject = project.Name
break
}
}
if len(devfileBytes) == 0 {
return nil, "", fmt.Errorf("could not find devfile in any project")
}

devfile = &dw.DevWorkspaceTemplateSpec{}
if err := yaml.Unmarshal(devfileBytes, devfile); err != nil {
return nil, "", fmt.Errorf("failed to read devfile in project %s: %s", devfileProject, err)
}
return devfile, devfileProject, nil
}

func getDevfileFromProject(project dw.Project) ([]byte, error) {
clonePath := projects.GetClonePath(&project)
for _, devfileName := range devfileNames {
if bytes, err := os.ReadFile(path.Join(internal.ProjectsRoot, clonePath, devfileName)); err == nil {
return bytes, nil
}
}
return nil, fmt.Errorf("no devfile found")
}
13 changes: 13 additions & 0 deletions project-clone/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (

projectslib "github.com/devfile/devworkspace-operator/pkg/library/projects"
"github.com/devfile/devworkspace-operator/project-clone/internal"
"github.com/devfile/devworkspace-operator/project-clone/internal/bootstrap"
"github.com/devfile/devworkspace-operator/project-clone/internal/git"
"github.com/devfile/devworkspace-operator/project-clone/internal/zip"
gitclient "github.com/go-git/go-git/v5/plumbing/transport/client"
Expand Down Expand Up @@ -114,6 +115,18 @@ func main() {
}
if encounteredError {
copyLogFileToProjectsRoot()
os.Exit(0)
}

needBootstrap, err := bootstrap.NeedsBootstrap(workspace)
if err != nil {
log.Printf("Encountered error reading DevWorkspace attributes: %s", err)
copyLogFileToProjectsRoot()
} else if needBootstrap {
if err := bootstrap.BootstrapWorkspace(workspace); err != nil {
log.Printf("Encountered error setting up DevWorkspace from devfile: %s", err)
copyLogFileToProjectsRoot()
}
}
}

Expand Down
Loading