From acc7b61a0e86418584208ca1d512e74d986ea646 Mon Sep 17 00:00:00 2001 From: Prakhar Agarwal Date: Wed, 1 Nov 2023 19:43:11 +0530 Subject: [PATCH] Add dockerfilegenerators (#9) * golangtransformer complete with transform code * fix: removed kubernetes dep * feat: add dockerfilegenerators --- common/utils.go | 149 +++ environment/container/container.go | 6 +- environment/container/dockerengine.go | 774 ++++++------- environment/container/fileinfo.go | 109 +- environment/container/utils.go | 195 ++-- go.mod | 15 +- go.sum | 19 +- .../dockerfilegenerator/dotnet/constants.go | 32 + .../dockerfilegenerator/dotnet/utils.go | 114 ++ .../dotnetcoredockerfilegenerator.go | 711 ++++++++++++ .../golangdockerfiletransformer.go | 2 - .../dockerfilegenerator/java/earanalyser.go | 118 ++ .../java/gradle/constants.go | 79 ++ .../java/gradle/gradleparser.go | 599 ++++++++++ .../java/gradle/quotedRegex.go | 61 + .../dockerfilegenerator/java/gradle/types.go | 136 +++ .../java/gradleanalyser.go | 1020 +++++++++++++++++ .../dockerfilegenerator/java/jaranalyser.go | 261 +++++ transformer/dockerfilegenerator/java/jboss.go | 221 ++++ .../dockerfilegenerator/java/liberty.go | 248 ++++ .../dockerfilegenerator/java/mavenanalyser.go | 732 ++++++++++++ .../java/springbootutils.go | 420 +++++++ .../dockerfilegenerator/java/tomcat.go | 215 ++++ transformer/dockerfilegenerator/java/types.go | 39 + transformer/dockerfilegenerator/java/utils.go | 81 ++ .../dockerfilegenerator/java/waranalyser.go | 113 ++ .../dockerfilegenerator/java/zuulanalyser.go | 110 ++ .../nodejsdockerfiletransformer.go | 329 ++++++ .../phpdockerfiletransformer.go | 264 +++++ .../pythondockerfiletransformer.go | 296 +++++ .../rubydockerfiletransformer.go | 167 +++ .../rustdockerfiletransformer.go | 200 ++++ transformer/dockerfilegenerator/utils.go | 46 + .../windows/consoleappdockerfilegenerator.go | 328 ++++++ .../silverlightwebappdockerfilegenerator.go | 224 ++++ .../dockerfilegenerator/windows/types.go | 41 + .../dockerfilegenerator/windows/utils.go | 59 + .../windows/webappdockerfilegenerator.go | 450 ++++++++ 38 files changed, 8432 insertions(+), 551 deletions(-) create mode 100644 transformer/dockerfilegenerator/dotnet/constants.go create mode 100644 transformer/dockerfilegenerator/dotnet/utils.go create mode 100644 transformer/dockerfilegenerator/dotnetcoredockerfilegenerator.go create mode 100644 transformer/dockerfilegenerator/java/earanalyser.go create mode 100644 transformer/dockerfilegenerator/java/gradle/constants.go create mode 100644 transformer/dockerfilegenerator/java/gradle/gradleparser.go create mode 100644 transformer/dockerfilegenerator/java/gradle/quotedRegex.go create mode 100644 transformer/dockerfilegenerator/java/gradle/types.go create mode 100644 transformer/dockerfilegenerator/java/gradleanalyser.go create mode 100644 transformer/dockerfilegenerator/java/jaranalyser.go create mode 100644 transformer/dockerfilegenerator/java/jboss.go create mode 100644 transformer/dockerfilegenerator/java/liberty.go create mode 100644 transformer/dockerfilegenerator/java/mavenanalyser.go create mode 100644 transformer/dockerfilegenerator/java/springbootutils.go create mode 100644 transformer/dockerfilegenerator/java/tomcat.go create mode 100644 transformer/dockerfilegenerator/java/types.go create mode 100644 transformer/dockerfilegenerator/java/utils.go create mode 100644 transformer/dockerfilegenerator/java/waranalyser.go create mode 100644 transformer/dockerfilegenerator/java/zuulanalyser.go create mode 100644 transformer/dockerfilegenerator/nodejsdockerfiletransformer.go create mode 100644 transformer/dockerfilegenerator/phpdockerfiletransformer.go create mode 100644 transformer/dockerfilegenerator/pythondockerfiletransformer.go create mode 100644 transformer/dockerfilegenerator/rubydockerfiletransformer.go create mode 100644 transformer/dockerfilegenerator/rustdockerfiletransformer.go create mode 100644 transformer/dockerfilegenerator/utils.go create mode 100644 transformer/dockerfilegenerator/windows/consoleappdockerfilegenerator.go create mode 100644 transformer/dockerfilegenerator/windows/silverlightwebappdockerfilegenerator.go create mode 100644 transformer/dockerfilegenerator/windows/types.go create mode 100644 transformer/dockerfilegenerator/windows/utils.go create mode 100644 transformer/dockerfilegenerator/windows/webappdockerfilegenerator.go diff --git a/common/utils.go b/common/utils.go index 93d96fc..3267cf1 100644 --- a/common/utils.go +++ b/common/utils.go @@ -30,6 +30,7 @@ import ( "github.com/docker/docker/pkg/ioutils" "github.com/konveyor/move2kube-wasm/types" "github.com/mitchellh/mapstructure" + "golang.org/x/text/transform" "gopkg.in/yaml.v3" "hash/crc64" "k8s.io/apimachinery/pkg/api/resource" @@ -47,6 +48,7 @@ import ( "strings" "github.com/sirupsen/logrus" + encodingunicode "golang.org/x/text/encoding/unicode" ) // ObjectToYamlBytes encodes an object to yaml @@ -921,3 +923,150 @@ func FindIndex[T interface{}](vs []T, condition func(T) bool) int { func JoinQASubKeys(xs ...string) string { return strings.Join(xs, Delim) } + +// StripQuotes strips a single layer of double or single quotes from the left and right ends +// Example: "github.com" -> github.com +// Example: 'github.com' -> github.com +// Example: "'github.com'" -> 'github.com' +func StripQuotes(s string) string { + if strings.HasPrefix(s, `"`) && strings.HasSuffix(s, `"`) { + return strings.TrimSuffix(strings.TrimPrefix(s, `"`), `"`) + } + if strings.HasPrefix(s, `'`) && strings.HasSuffix(s, `'`) { + return strings.TrimSuffix(strings.TrimPrefix(s, `'`), `'`) + } + return s +} + +// GetFilesInCurrentDirectory returns the name of the file present in the current directory which matches the pattern +func GetFilesInCurrentDirectory(path string, fileNames, fileNameRegexes []string) (matchedFilePaths []string, err error) { + matchedFilePaths = []string{} + currFileNames := []string{} + info, err := os.Stat(path) + if err != nil { + return nil, fmt.Errorf("failed to stat the directory at path %s . Error: %q", path, err) + } + if !info.IsDir() { + logrus.Warnf("the provided path %s is not a directory. info: %+v", path, info) + currFileNames = append(currFileNames, path) + } else { + dirHandle, err := os.Open(path) + if err != nil { + return nil, fmt.Errorf("failed to open the directory %s . Error: %q", path, err) + } + defer dirHandle.Close() + currFileNames, err = dirHandle.Readdirnames(0) // 0 to read all files and folders + if err != nil { + return nil, fmt.Errorf("failed to get the list of files in the directory %s . Error: %q", path, err) + } + } + compiledNameRegexes := []*regexp.Regexp{} + for _, nameRegex := range fileNameRegexes { + compiledNameRegex, err := regexp.Compile(nameRegex) + if err != nil { + logrus.Errorf("skipping because the regular expression `%s` failed to compile. Error: %q", nameRegex, err) + continue + } + compiledNameRegexes = append(compiledNameRegexes, compiledNameRegex) + } + for _, currFileName := range currFileNames { + for _, fileName := range fileNames { + if fileName == currFileName { + matchedFilePaths = append(matchedFilePaths, filepath.Join(path, currFileName)) + break + } + } + for _, compiledNameRegex := range compiledNameRegexes { + if compiledNameRegex.MatchString(currFileName) { + matchedFilePaths = append(matchedFilePaths, filepath.Join(path, currFileName)) + break + } + } + } + return matchedFilePaths, nil +} + +// GetFilesByExtInCurrDir returns the files present in current directory which have one of the specified extensions +func GetFilesByExtInCurrDir(dir string, exts []string) ([]string, error) { + var files []string + info, err := os.Stat(dir) + if err != nil { + return nil, fmt.Errorf("failed to stat the directory '%s' . Error: %w", dir, err) + } + if !info.IsDir() { + logrus.Warnf("the provided path '%s' is not a directory", dir) + fext := filepath.Ext(dir) + for _, ext := range exts { + if fext == ext { + return []string{dir}, nil + } + } + return nil, nil + } + dirEntries, err := os.ReadDir(dir) + if err != nil { + return nil, fmt.Errorf("failed to read the directory '%s' . Error: %w", dir, err) + } + for _, de := range dirEntries { + if de.IsDir() { + continue + } + fext := filepath.Ext(de.Name()) + for _, ext := range exts { + if fext == ext { + files = append(files, filepath.Join(dir, de.Name())) + break + } + } + } + return files, nil +} + +// ConvertUtf8AndUtf16ToUtf8 converts UTF-8 and UTF-16 encoded text (with or without a BOM) into UTF-8 encoded text (without a BOM) +func ConvertUtf8AndUtf16ToUtf8(original []byte) ([]byte, error) { + utf8and16 := encodingunicode.BOMOverride(encodingunicode.UTF8.NewDecoder()) + buf := &bytes.Buffer{} + w1 := transform.NewWriter(buf, utf8and16) + if _, err := w1.Write(original); err != nil { + return nil, fmt.Errorf("failed to transform the bytes to utf-8. Error: %w\nOriginal bytes: %+v", err, original) + } + err := w1.Close() + return buf.Bytes(), err +} + +// ReadJSON reads an json into an object +func ReadJSON(path string, data interface{}) error { + jsonBytes, err := os.ReadFile(path) + if err != nil { + return fmt.Errorf("failed to read the json file at path '%s' . Error: %w", path, err) + } + jsonUtf8Bytes, err := ConvertUtf8AndUtf16ToUtf8(jsonBytes) + if err != nil { + return fmt.Errorf("failed to convert the json file at path '%s' to utf-8. Error: %w", path, err) + } + if err := json.Unmarshal(jsonUtf8Bytes, &data); err != nil { + return fmt.Errorf("failed to parse the json file at path '%s' . Error: %w\nBytes before transform: %+v\nBytes after transform: %+v", path, err, jsonBytes, jsonUtf8Bytes) + } + return nil +} + +// Filter returns the elements that satisfy the condition. +// It returns nil if none of the elements satisfy the condition. +func Filter[T comparable](vs []T, condition func(T) bool) []T { + var ws []T + for _, v := range vs { + if condition(v) { + ws = append(ws, v) + } + } + return ws +} + +// Map applies the given function over all the elements and returns a new slice with the results. +func Map[T1 interface{}, T2 interface{}](vs []T1, f func(T1) T2) []T2 { + var ws []T2 + for _, v := range vs { + ws = append(ws, f(v)) + } + return ws +} diff --git a/environment/container/container.go b/environment/container/container.go index a282f13..7c03bf5 100644 --- a/environment/container/container.go +++ b/environment/container/container.go @@ -19,7 +19,7 @@ package container import ( "errors" "fmt" - dockertypes "github.com/docker/docker/api/types" + //dockertypes "github.com/docker/docker/api/types" environmenttypes "github.com/konveyor/move2kube-wasm/types/environment" "github.com/sirupsen/logrus" "io/fs" @@ -38,7 +38,9 @@ type ContainerEngine interface { // RunCmdInContainer runs a container RunCmdInContainer(image string, cmd environmenttypes.Command, workingdir string, env []string) (stdout, stderr string, exitcode int, err error) // InspectImage gets Inspect output for a container - InspectImage(image string) (dockertypes.ImageInspect, error) + //TODO: WASI + + //InspectImage(image string) (dockertypes.ImageInspect, error) // TODO: Change paths from map to array CopyDirsIntoImage(image, newImageName string, paths map[string]string) (err error) CopyDirsIntoContainer(containerID string, paths map[string]string) (err error) diff --git a/environment/container/dockerengine.go b/environment/container/dockerengine.go index 7257d33..befbe67 100644 --- a/environment/container/dockerengine.go +++ b/environment/container/dockerengine.go @@ -1,391 +1,393 @@ /* - * Copyright IBM Corporation 2021 - * - * 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. +* Copyright IBM Corporation 2021 +* +* 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 container -import ( - "bufio" - "bytes" - "context" - "fmt" - "io" - "io/fs" - "strings" - - "github.com/docker/docker/api/types" - containertypes "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/mount" - "github.com/docker/docker/client" - "github.com/docker/docker/pkg/stdcopy" - - // "github.com/docker/docker/pkg/stdcopy" - "github.com/konveyor/move2kube-wasm/common" - environmenttypes "github.com/konveyor/move2kube-wasm/types/environment" - "github.com/sirupsen/logrus" - "github.com/spf13/cast" -) - -const ( - testimage = "quay.io/konveyor/hello-world" -) - -type dockerEngine struct { - availableImages map[string]bool - cli *client.Client - ctx context.Context -} - -// newDockerEngine creates a new docker engine instance -func newDockerEngine() (*dockerEngine, error) { - ctx := context.Background() - cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) - if err != nil { - return nil, fmt.Errorf("failed to create the docker client. Error: %w", err) - } - engine := &dockerEngine{ - cli: cli, - ctx: ctx, - } - if err := engine.updateAvailableImages(); err != nil { - return engine, fmt.Errorf("failed to update the list of available images. Error: %w", err) - } - if _, _, err := engine.RunContainer(testimage, environmenttypes.Command{}, "", ""); err != nil { - return engine, fmt.Errorf("failed to run the test image '%s' as a container. Error: %w", testimage, err) - } - return engine, nil -} - -// updateAvailableImages updates the list of available images using the local cache (docker images) -func (e *dockerEngine) updateAvailableImages() error { - images, err := e.cli.ImageList(e.ctx, types.ImageListOptions{All: true}) - if err != nil { - return fmt.Errorf("failed to list the images. Error: %w", err) - } - e.availableImages = map[string]bool{} - for _, image := range images { - for _, repoTag := range image.RepoTags { - e.availableImages[repoTag] = true - } - } - return nil -} - -func (e *dockerEngine) pullImage(image string) error { - if e.availableImages == nil { - return fmt.Errorf("the Docker engine has not been initialized. The list of available images is nil") - } - if _, ok := e.availableImages[image]; ok { - return nil - } - logrus.Infof("Pulling container image %s. This could take a few mins.", image) - out, err := e.cli.ImagePull(e.ctx, image, types.ImagePullOptions{}) - if err != nil { - e.availableImages[image] = false - return fmt.Errorf("failed to pull the image '%s' using the docker client. Error: %q", image, err) - } - if b, err := io.ReadAll(out); err == nil { - logrus.Debug(cast.ToString(b)) - } - e.availableImages[image] = true - return nil -} - -// RunCmdInContainer executes a container -func (e *dockerEngine) RunCmdInContainer(containerID string, cmd environmenttypes.Command, workingdir string, env []string) (stdout, stderr string, exitCode int, err error) { - execConfig := types.ExecConfig{ - AttachStdout: true, - AttachStderr: true, - Cmd: cmd, - WorkingDir: workingdir, - Env: env, - } - cresp, err := e.cli.ContainerExecCreate(e.ctx, containerID, execConfig) - if err != nil { - return "", "", -1, fmt.Errorf("failed to execute a process in the container. Error: %w", err) - } - aresp, err := e.cli.ContainerExecAttach(e.ctx, cresp.ID, types.ExecStartCheck{}) - if err != nil { - return "", "", -1, fmt.Errorf("failed to execute a process in the container and attach to it. Error: %w", err) - } - defer aresp.Close() - - var outBuf, errBuf bytes.Buffer - outputDone := make(chan error) - - // log the container output so we can see what's happening for long running tasks - ff, err := bufio.NewReader(aresp.Reader).ReadString('\n') - buf := &bytes.Buffer{} - for err == nil { - buf.Write([]byte(ff)) - logrus.Debugf("msg from cmd running in the container is: %s", ff) - ff, err = bufio.NewReader(aresp.Reader).ReadString('\n') - } - if err == io.EOF && len(ff) > 0 { - buf.Write([]byte(ff)) - } - - go func() { - _, err = stdcopy.StdCopy(&outBuf, &errBuf, buf) - outputDone <- err - }() - - select { - case err = <-outputDone: - if err != nil { - return - } - break - - case <-e.ctx.Done(): - return "", "", 0, e.ctx.Err() - } - - stdoutbytes := outBuf.Bytes() - stderrbytes := errBuf.Bytes() - res, err := e.cli.ContainerExecInspect(e.ctx, cresp.ID) - if err != nil { - return - } - exitCode = res.ExitCode - stdout = string(stdoutbytes) - stderr = string(stderrbytes) - return -} - -// InspectImage returns inspect output for an image -func (e *dockerEngine) InspectImage(image string) (types.ImageInspect, error) { - inspectOutput, _, err := e.cli.ImageInspectWithRaw(e.ctx, image) - if err != nil { - return types.ImageInspect{}, err - } - - return inspectOutput, nil -} - -// CreateContainer creates a container -func (e *dockerEngine) CreateContainer(container environmenttypes.Container) (containerid string, err error) { - if err := e.pullImage(container.Image); err != nil { - return "", fmt.Errorf("failed to pull the image '%s'. Error: %w", container.Image, err) - } - contconfig := &containertypes.Config{ - Image: container.Image, - } - if len(container.KeepAliveCommand) > 0 { - contconfig.Cmd = container.KeepAliveCommand - } - resp, err := e.cli.ContainerCreate(e.ctx, contconfig, nil, nil, nil, "") - if err != nil { - return "", fmt.Errorf("failed to create the container with the image '%s' and no volumes attached. Error: %w", container.Image, err) - } - if err := e.cli.ContainerStart(e.ctx, resp.ID, types.ContainerStartOptions{}); err != nil { - return "", fmt.Errorf("failed to start the container with the ID '%s', image '%s' and no volumes attached. Error: %w", resp.ID, container.Image, err) - } - logrus.Debugf("Container with ID '%s' created with the image '%s'", resp.ID, container.Image) - return resp.ID, nil -} - -// StopAndRemoveContainer stops and removes a running container -func (e *dockerEngine) StopAndRemoveContainer(containerID string) error { - if err := e.cli.ContainerRemove(e.ctx, containerID, types.ContainerRemoveOptions{Force: true}); err != nil { - return fmt.Errorf("failed to remove the container with ID '%s' . Error: %w", containerID, err) - } - return nil -} - -// CopyDirsIntoImage copies some directories into a container -func (e *dockerEngine) CopyDirsIntoImage(image, newImageName string, paths map[string]string) (err error) { - logrus.Trace("CopyDirsIntoImage start") - defer logrus.Trace("CopyDirsIntoImage end") - if err := e.pullImage(image); err != nil { - return fmt.Errorf("failed to pull the image '%s'. Error: %w", image, err) - } - cid, err := e.CreateContainer(environmenttypes.Container{Image: image}) - if err != nil { - return fmt.Errorf("failed to create container with base image '%s' . Error: %w", image, err) - } - for sp, dp := range paths { - if err := copyDirToContainer(e.ctx, e.cli, cid, sp, dp); err != nil { - return fmt.Errorf("container data copy failed for image '%s' with volume %s:%s . Error: %w", image, sp, dp, err) - } - } - if _, err := e.cli.ContainerCommit(e.ctx, cid, types.ContainerCommitOptions{Reference: newImageName}); err != nil { - return fmt.Errorf("failed to commit the container with the input data as a new image. Error: %w", err) - } - e.availableImages[newImageName] = true - if err := e.StopAndRemoveContainer(cid); err != nil { - return fmt.Errorf("failed to stop and remove container with id '%s' . Error: %w", cid, err) - } - return nil -} - -// CopyDirsIntoContainer copies some directories into a container -func (e *dockerEngine) CopyDirsIntoContainer(containerID string, paths map[string]string) (err error) { - for sp, dp := range paths { - err = copyDirToContainer(e.ctx, e.cli, containerID, sp, dp) - if err != nil { - return fmt.Errorf("container data copy failed for image '%s' with volume %s:%s . Error: %w", containerID, sp, dp, err) - } - } - return nil -} - -// Stat a container's info by the container id -func (e *dockerEngine) Stat(containerID string, name string) (fs.FileInfo, error) { - stat, err := e.cli.ContainerStatPath(e.ctx, containerID, name) - if err != nil { - return nil, err - } - return &FileInfo{ - stat: stat, - }, err -} - -// CopyDirsFromContainer copies a directory from inside the container -func (e *dockerEngine) CopyDirsFromContainer(containerID string, paths map[string]string) (err error) { - for sp, dp := range paths { - if err := copyFromContainer(e.ctx, e.cli, containerID, sp, dp); err != nil { - return fmt.Errorf("failed to copy data from the container with ID '%s' from source path '%s' to destination path '%s' . Error: %w", containerID, sp, dp, err) - } - } - return nil -} - -// BuildImage builds a container image -func (e *dockerEngine) BuildImage(image, context, dockerfile string) (err error) { - logrus.Infof("Building container image '%s' . This could take a few mins.", image) - reader := common.ReadFilesAsTar(context, "", common.NoCompression) - resp, err := e.cli.ImageBuild(e.ctx, reader, types.ImageBuildOptions{ - Dockerfile: dockerfile, - Tags: []string{image}, - }) - if err != nil { - return fmt.Errorf("image creation failed with image '%s' with no volumes. Error: %w", image, err) - } - defer resp.Body.Close() - response, err := io.ReadAll(resp.Body) - if err != nil { - return fmt.Errorf("failed to read data from image build process. Error: %w", err) - } - logrus.Debugf("%s", response) - e.availableImages[image] = true - logrus.Debugf("Built image %s", image) - return nil -} - -// RemoveImage removes a container image -func (e *dockerEngine) RemoveImage(image string) (err error) { - _, err = e.cli.ImageRemove(e.ctx, image, types.ImageRemoveOptions{Force: true}) - if err != nil { - return fmt.Errorf("container deletion failed with image '%s' . Error: %w", image, err) - } - return nil -} - -// RunContainer executes a container -func (e *dockerEngine) RunContainer(image string, cmd environmenttypes.Command, volsrc string, voldest string) (output string, containerStarted bool, err error) { - if err := e.pullImage(image); err != nil { - return "", false, fmt.Errorf("failed to pull the image '%s'. Error: %w", image, err) - } - ctx := context.Background() - cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) - if err != nil { - return "", false, fmt.Errorf("failed to create a docker client. Error: %w", err) - } - contconfig := &containertypes.Config{Image: image} - if (volsrc == "" && voldest != "") || (volsrc != "" && voldest == "") { - logrus.Warnf("Either volume source (%s) or destination (%s) is empty. Ingoring volume mount.", volsrc, voldest) - } - hostconfig := &containertypes.HostConfig{} - if volsrc != "" && voldest != "" { - hostconfig.Mounts = []mount.Mount{ - { - Type: mount.TypeBind, - Source: volsrc, - Target: voldest, - ReadOnly: true, - }, - } - } - resp, err := cli.ContainerCreate(ctx, contconfig, hostconfig, nil, nil, "") - if err != nil { - logrus.Debugf("failed to create the container with contconfig %+v and hostconfig %+v . Error: %q", contconfig, hostconfig, err) - resp, err = cli.ContainerCreate(ctx, contconfig, nil, nil, nil, "") - if err != nil { - return "", false, fmt.Errorf("container creation failed for image '%s' with no volumes", image) - } - logrus.Debugf("Container %s created with image %s with no volumes", resp.ID, image) - defer cli.ContainerRemove(ctx, resp.ID, types.ContainerRemoveOptions{Force: true}) - if volsrc != "" && voldest != "" { - err = copyDir(ctx, cli, resp.ID, volsrc, voldest) - if err != nil { - return "", false, fmt.Errorf("container data copy failed for image '%s' with volume (%s:%s). Error: %w", image, volsrc, voldest, err) - } - logrus.Debugf("Data copied from (%s) to (%s) in container '%s' with image '%s'", volsrc, voldest, resp.ID, image) - } - } - logrus.Debugf("Container %s created with image %s", resp.ID, image) - defer cli.ContainerRemove(ctx, resp.ID, types.ContainerRemoveOptions{Force: true}) - if err := cli.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{}); err != nil { - return "", false, fmt.Errorf("failed to startup the container '%s' . Error: %w", resp.ID, err) - } - statusCh, errCh := cli.ContainerWait( - ctx, - resp.ID, - containertypes.WaitConditionNotRunning, - ) - containerLogsStream, err := cli.ContainerLogs(ctx, resp.ID, types.ContainerLogsOptions{ShowStdout: true}) - if err != nil { - return "", false, fmt.Errorf("failed to get the container logs. Error: %w", err) - } - containerLogs := bufio.NewReader(containerLogsStream) - go func() { - defer containerLogsStream.Close() - text, err := containerLogs.ReadString('\n') - for err == nil { - updatedText := strings.TrimSpace(text) - if updatedText != "" { - logrus.Debugf("msg from container is: %s", updatedText) - } - text, err = containerLogs.ReadString('\n') - } - if err != nil { - logrus.Debugf("container msg loop ended. Error: %q", err) - } - }() - select { - case err := <-errCh: - if err != nil { - return "", false, fmt.Errorf("error while waiting for container. Error: %w", err) - } - case status := <-statusCh: - logrus.Debugf("Container exited with status code: %#+v", status.StatusCode) - options := types.ContainerLogsOptions{ShowStdout: true} - out, err := cli.ContainerLogs(ctx, resp.ID, options) - if err != nil { - logrus.Debugf("Error while getting container logs. Error: %q", err) - return "", true, err - } - logs := "" - if b, err := io.ReadAll(out); err == nil { - logs = cast.ToString(b) - } - if status.StatusCode != 0 { - return logs, true, fmt.Errorf("container execution terminated with error code: %d", status.StatusCode) - } - return logs, true, nil - } - return "", false, nil -} +// +//import ( +// "bufio" +// "bytes" +// "context" +// "fmt" +// "github.com/docker/docker/api/types" +// "io" +// "io/fs" +// "strings" +// +// //"github.com/docker/docker/api/types" +// //containertypes "github.com/docker/docker/api/types/container" +// "github.com/docker/docker/api/types/mount" +// "github.com/docker/docker/client" +// "github.com/docker/docker/pkg/stdcopy" +// +// // "github.com/docker/docker/pkg/stdcopy" +// "github.com/konveyor/move2kube-wasm/common" +// environmenttypes "github.com/konveyor/move2kube-wasm/types/environment" +// "github.com/sirupsen/logrus" +// "github.com/spf13/cast" +//) +// +//const ( +// testimage = "quay.io/konveyor/hello-world" +//) +// +//type dockerEngine struct { +// availableImages map[string]bool +// cli *client.Client +// ctx context.Context +//} +// +//// newDockerEngine creates a new docker engine instance +//func newDockerEngine() (*dockerEngine, error) { +// ctx := context.Background() +// cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) +// if err != nil { +// return nil, fmt.Errorf("failed to create the docker client. Error: %w", err) +// } +// engine := &dockerEngine{ +// cli: cli, +// ctx: ctx, +// } +// if err := engine.updateAvailableImages(); err != nil { +// return engine, fmt.Errorf("failed to update the list of available images. Error: %w", err) +// } +// if _, _, err := engine.RunContainer(testimage, environmenttypes.Command{}, "", ""); err != nil { +// return engine, fmt.Errorf("failed to run the test image '%s' as a container. Error: %w", testimage, err) +// } +// return engine, nil +//} +// +//// updateAvailableImages updates the list of available images using the local cache (docker images) +//func (e *dockerEngine) updateAvailableImages() error { +// images, err := e.cli.ImageList(e.ctx, types.ImageListOptions{All: true}) +// if err != nil { +// return fmt.Errorf("failed to list the images. Error: %w", err) +// } +// e.availableImages = map[string]bool{} +// for _, image := range images { +// for _, repoTag := range image.RepoTags { +// e.availableImages[repoTag] = true +// } +// } +// return nil +//} +// +//func (e *dockerEngine) pullImage(image string) error { +// if e.availableImages == nil { +// return fmt.Errorf("the Docker engine has not been initialized. The list of available images is nil") +// } +// if _, ok := e.availableImages[image]; ok { +// return nil +// } +// logrus.Infof("Pulling container image %s. This could take a few mins.", image) +// out, err := e.cli.ImagePull(e.ctx, image, types.ImagePullOptions{}) +// if err != nil { +// e.availableImages[image] = false +// return fmt.Errorf("failed to pull the image '%s' using the docker client. Error: %q", image, err) +// } +// if b, err := io.ReadAll(out); err == nil { +// logrus.Debug(cast.ToString(b)) +// } +// e.availableImages[image] = true +// return nil +//} +// +//// RunCmdInContainer executes a container +//func (e *dockerEngine) RunCmdInContainer(containerID string, cmd environmenttypes.Command, workingdir string, env []string) (stdout, stderr string, exitCode int, err error) { +// execConfig := types.ExecConfig{ +// AttachStdout: true, +// AttachStderr: true, +// Cmd: cmd, +// WorkingDir: workingdir, +// Env: env, +// } +// cresp, err := e.cli.ContainerExecCreate(e.ctx, containerID, execConfig) +// if err != nil { +// return "", "", -1, fmt.Errorf("failed to execute a process in the container. Error: %w", err) +// } +// aresp, err := e.cli.ContainerExecAttach(e.ctx, cresp.ID, types.ExecStartCheck{}) +// if err != nil { +// return "", "", -1, fmt.Errorf("failed to execute a process in the container and attach to it. Error: %w", err) +// } +// defer aresp.Close() +// +// var outBuf, errBuf bytes.Buffer +// outputDone := make(chan error) +// +// // log the container output so we can see what's happening for long running tasks +// ff, err := bufio.NewReader(aresp.Reader).ReadString('\n') +// buf := &bytes.Buffer{} +// for err == nil { +// buf.Write([]byte(ff)) +// logrus.Debugf("msg from cmd running in the container is: %s", ff) +// ff, err = bufio.NewReader(aresp.Reader).ReadString('\n') +// } +// if err == io.EOF && len(ff) > 0 { +// buf.Write([]byte(ff)) +// } +// +// go func() { +// _, err = stdcopy.StdCopy(&outBuf, &errBuf, buf) +// outputDone <- err +// }() +// +// select { +// case err = <-outputDone: +// if err != nil { +// return +// } +// break +// +// case <-e.ctx.Done(): +// return "", "", 0, e.ctx.Err() +// } +// +// stdoutbytes := outBuf.Bytes() +// stderrbytes := errBuf.Bytes() +// res, err := e.cli.ContainerExecInspect(e.ctx, cresp.ID) +// if err != nil { +// return +// } +// exitCode = res.ExitCode +// stdout = string(stdoutbytes) +// stderr = string(stderrbytes) +// return +//} +// +//// InspectImage returns inspect output for an image +//func (e *dockerEngine) InspectImage(image string) (types.ImageInspect, error) { +// inspectOutput, _, err := e.cli.ImageInspectWithRaw(e.ctx, image) +// if err != nil { +// return types.ImageInspect{}, err +// } +// +// return inspectOutput, nil +//} +// +//// CreateContainer creates a container +//func (e *dockerEngine) CreateContainer(container environmenttypes.Container) (containerid string, err error) { +// if err := e.pullImage(container.Image); err != nil { +// return "", fmt.Errorf("failed to pull the image '%s'. Error: %w", container.Image, err) +// } +// contconfig := &containertypes.Config{ +// Image: container.Image, +// } +// if len(container.KeepAliveCommand) > 0 { +// contconfig.Cmd = container.KeepAliveCommand +// } +// resp, err := e.cli.ContainerCreate(e.ctx, contconfig, nil, nil, nil, "") +// if err != nil { +// return "", fmt.Errorf("failed to create the container with the image '%s' and no volumes attached. Error: %w", container.Image, err) +// } +// if err := e.cli.ContainerStart(e.ctx, resp.ID, types.ContainerStartOptions{}); err != nil { +// return "", fmt.Errorf("failed to start the container with the ID '%s', image '%s' and no volumes attached. Error: %w", resp.ID, container.Image, err) +// } +// logrus.Debugf("Container with ID '%s' created with the image '%s'", resp.ID, container.Image) +// return resp.ID, nil +//} +// +//// StopAndRemoveContainer stops and removes a running container +//func (e *dockerEngine) StopAndRemoveContainer(containerID string) error { +// if err := e.cli.ContainerRemove(e.ctx, containerID, types.ContainerRemoveOptions{Force: true}); err != nil { +// return fmt.Errorf("failed to remove the container with ID '%s' . Error: %w", containerID, err) +// } +// return nil +//} +// +//// CopyDirsIntoImage copies some directories into a container +//func (e *dockerEngine) CopyDirsIntoImage(image, newImageName string, paths map[string]string) (err error) { +// logrus.Trace("CopyDirsIntoImage start") +// defer logrus.Trace("CopyDirsIntoImage end") +// if err := e.pullImage(image); err != nil { +// return fmt.Errorf("failed to pull the image '%s'. Error: %w", image, err) +// } +// cid, err := e.CreateContainer(environmenttypes.Container{Image: image}) +// if err != nil { +// return fmt.Errorf("failed to create container with base image '%s' . Error: %w", image, err) +// } +// for sp, dp := range paths { +// if err := copyDirToContainer(e.ctx, e.cli, cid, sp, dp); err != nil { +// return fmt.Errorf("container data copy failed for image '%s' with volume %s:%s . Error: %w", image, sp, dp, err) +// } +// } +// if _, err := e.cli.ContainerCommit(e.ctx, cid, types.ContainerCommitOptions{Reference: newImageName}); err != nil { +// return fmt.Errorf("failed to commit the container with the input data as a new image. Error: %w", err) +// } +// e.availableImages[newImageName] = true +// if err := e.StopAndRemoveContainer(cid); err != nil { +// return fmt.Errorf("failed to stop and remove container with id '%s' . Error: %w", cid, err) +// } +// return nil +//} +// +//// CopyDirsIntoContainer copies some directories into a container +//func (e *dockerEngine) CopyDirsIntoContainer(containerID string, paths map[string]string) (err error) { +// for sp, dp := range paths { +// err = copyDirToContainer(e.ctx, e.cli, containerID, sp, dp) +// if err != nil { +// return fmt.Errorf("container data copy failed for image '%s' with volume %s:%s . Error: %w", containerID, sp, dp, err) +// } +// } +// return nil +//} +// +//// Stat a container's info by the container id +//func (e *dockerEngine) Stat(containerID string, name string) (fs.FileInfo, error) { +// stat, err := e.cli.ContainerStatPath(e.ctx, containerID, name) +// if err != nil { +// return nil, err +// } +// return &FileInfo{ +// stat: stat, +// }, err +//} +// +//// CopyDirsFromContainer copies a directory from inside the container +//func (e *dockerEngine) CopyDirsFromContainer(containerID string, paths map[string]string) (err error) { +// for sp, dp := range paths { +// if err := copyFromContainer(e.ctx, e.cli, containerID, sp, dp); err != nil { +// return fmt.Errorf("failed to copy data from the container with ID '%s' from source path '%s' to destination path '%s' . Error: %w", containerID, sp, dp, err) +// } +// } +// return nil +//} +// +//// BuildImage builds a container image +//func (e *dockerEngine) BuildImage(image, context, dockerfile string) (err error) { +// logrus.Infof("Building container image '%s' . This could take a few mins.", image) +// reader := common.ReadFilesAsTar(context, "", common.NoCompression) +// resp, err := e.cli.ImageBuild(e.ctx, reader, types.ImageBuildOptions{ +// Dockerfile: dockerfile, +// Tags: []string{image}, +// }) +// if err != nil { +// return fmt.Errorf("image creation failed with image '%s' with no volumes. Error: %w", image, err) +// } +// defer resp.Body.Close() +// response, err := io.ReadAll(resp.Body) +// if err != nil { +// return fmt.Errorf("failed to read data from image build process. Error: %w", err) +// } +// logrus.Debugf("%s", response) +// e.availableImages[image] = true +// logrus.Debugf("Built image %s", image) +// return nil +//} +// +//// RemoveImage removes a container image +//func (e *dockerEngine) RemoveImage(image string) (err error) { +// _, err = e.cli.ImageRemove(e.ctx, image, types.ImageRemoveOptions{Force: true}) +// if err != nil { +// return fmt.Errorf("container deletion failed with image '%s' . Error: %w", image, err) +// } +// return nil +//} +// +//// RunContainer executes a container +//func (e *dockerEngine) RunContainer(image string, cmd environmenttypes.Command, volsrc string, voldest string) (output string, containerStarted bool, err error) { +// if err := e.pullImage(image); err != nil { +// return "", false, fmt.Errorf("failed to pull the image '%s'. Error: %w", image, err) +// } +// ctx := context.Background() +// cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) +// if err != nil { +// return "", false, fmt.Errorf("failed to create a docker client. Error: %w", err) +// } +// contconfig := &containertypes.Config{Image: image} +// if (volsrc == "" && voldest != "") || (volsrc != "" && voldest == "") { +// logrus.Warnf("Either volume source (%s) or destination (%s) is empty. Ingoring volume mount.", volsrc, voldest) +// } +// hostconfig := &containertypes.HostConfig{} +// if volsrc != "" && voldest != "" { +// hostconfig.Mounts = []mount.Mount{ +// { +// Type: mount.TypeBind, +// Source: volsrc, +// Target: voldest, +// ReadOnly: true, +// }, +// } +// } +// resp, err := cli.ContainerCreate(ctx, contconfig, hostconfig, nil, nil, "") +// if err != nil { +// logrus.Debugf("failed to create the container with contconfig %+v and hostconfig %+v . Error: %q", contconfig, hostconfig, err) +// resp, err = cli.ContainerCreate(ctx, contconfig, nil, nil, nil, "") +// if err != nil { +// return "", false, fmt.Errorf("container creation failed for image '%s' with no volumes", image) +// } +// logrus.Debugf("Container %s created with image %s with no volumes", resp.ID, image) +// defer cli.ContainerRemove(ctx, resp.ID, types.ContainerRemoveOptions{Force: true}) +// if volsrc != "" && voldest != "" { +// err = copyDir(ctx, cli, resp.ID, volsrc, voldest) +// if err != nil { +// return "", false, fmt.Errorf("container data copy failed for image '%s' with volume (%s:%s). Error: %w", image, volsrc, voldest, err) +// } +// logrus.Debugf("Data copied from (%s) to (%s) in container '%s' with image '%s'", volsrc, voldest, resp.ID, image) +// } +// } +// logrus.Debugf("Container %s created with image %s", resp.ID, image) +// defer cli.ContainerRemove(ctx, resp.ID, types.ContainerRemoveOptions{Force: true}) +// if err := cli.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{}); err != nil { +// return "", false, fmt.Errorf("failed to startup the container '%s' . Error: %w", resp.ID, err) +// } +// statusCh, errCh := cli.ContainerWait( +// ctx, +// resp.ID, +// containertypes.WaitConditionNotRunning, +// ) +// containerLogsStream, err := cli.ContainerLogs(ctx, resp.ID, types.ContainerLogsOptions{ShowStdout: true}) +// if err != nil { +// return "", false, fmt.Errorf("failed to get the container logs. Error: %w", err) +// } +// containerLogs := bufio.NewReader(containerLogsStream) +// go func() { +// defer containerLogsStream.Close() +// text, err := containerLogs.ReadString('\n') +// for err == nil { +// updatedText := strings.TrimSpace(text) +// if updatedText != "" { +// logrus.Debugf("msg from container is: %s", updatedText) +// } +// text, err = containerLogs.ReadString('\n') +// } +// if err != nil { +// logrus.Debugf("container msg loop ended. Error: %q", err) +// } +// }() +// select { +// case err := <-errCh: +// if err != nil { +// return "", false, fmt.Errorf("error while waiting for container. Error: %w", err) +// } +// case status := <-statusCh: +// logrus.Debugf("Container exited with status code: %#+v", status.StatusCode) +// options := types.ContainerLogsOptions{ShowStdout: true} +// out, err := cli.ContainerLogs(ctx, resp.ID, options) +// if err != nil { +// logrus.Debugf("Error while getting container logs. Error: %q", err) +// return "", true, err +// } +// logs := "" +// if b, err := io.ReadAll(out); err == nil { +// logs = cast.ToString(b) +// } +// if status.StatusCode != 0 { +// return logs, true, fmt.Errorf("container execution terminated with error code: %d", status.StatusCode) +// } +// return logs, true, nil +// } +// return "", false, nil +//} diff --git a/environment/container/fileinfo.go b/environment/container/fileinfo.go index f949669..c7d3f3b 100644 --- a/environment/container/fileinfo.go +++ b/environment/container/fileinfo.go @@ -1,59 +1,60 @@ /* - * Copyright IBM Corporation 2020, 2021 - * - * 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. +* Copyright IBM Corporation 2020, 2021 +* +* 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 container -import ( - "io/fs" - "time" - - "github.com/docker/docker/api/types" -) - -// FileInfo implements fs.FileInfo interface -type FileInfo struct { - stat types.ContainerPathStat -} - -// Name implements fs.FileInfo interface -func (f *FileInfo) Name() string { // base name of the file - return f.stat.Name -} - -// Size implements fs.FileInfo interface -func (f *FileInfo) Size() int64 { // length in bytes for regular files; system-dependent for others - return f.stat.Size -} - -// Mode implements fs.FileInfo interface -func (f *FileInfo) Mode() fs.FileMode { // file mode bits - return f.stat.Mode -} - -// ModTime implements fs.FileInfo interface -func (f *FileInfo) ModTime() time.Time { // modification time - return f.stat.Mtime -} - -// IsDir implements fs.FileInfo interface -func (f *FileInfo) IsDir() bool { // abbreviation for Mode().IsDir() - return f.stat.Mode.IsDir() -} - -// Sys implements fs.FileInfo interface -func (f *FileInfo) Sys() interface{} { // underlying data source (can return nil) - return nil -} +// +//import ( +// "io/fs" +// "time" +// +// "github.com/docker/docker/api/types" +//) +// +//// FileInfo implements fs.FileInfo interface +//type FileInfo struct { +// stat types.ContainerPathStat +//} +// +//// Name implements fs.FileInfo interface +//func (f *FileInfo) Name() string { // base name of the file +// return f.stat.Name +//} +// +//// Size implements fs.FileInfo interface +//func (f *FileInfo) Size() int64 { // length in bytes for regular files; system-dependent for others +// return f.stat.Size +//} +// +//// Mode implements fs.FileInfo interface +//func (f *FileInfo) Mode() fs.FileMode { // file mode bits +// return f.stat.Mode +//} +// +//// ModTime implements fs.FileInfo interface +//func (f *FileInfo) ModTime() time.Time { // modification time +// return f.stat.Mtime +//} +// +//// IsDir implements fs.FileInfo interface +//func (f *FileInfo) IsDir() bool { // abbreviation for Mode().IsDir() +// return f.stat.Mode.IsDir() +//} +// +//// Sys implements fs.FileInfo interface +//func (f *FileInfo) Sys() interface{} { // underlying data source (can return nil) +// return nil +//} diff --git a/environment/container/utils.go b/environment/container/utils.go index 84b938f..3019e1a 100644 --- a/environment/container/utils.go +++ b/environment/container/utils.go @@ -1,102 +1,103 @@ /* - * Copyright IBM Corporation 2020, 2021 - * - * 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. +* Copyright IBM Corporation 2020, 2021 +* +* 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 container -import ( - "context" - "fmt" - "io" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/client" - "github.com/docker/docker/pkg/archive" - "github.com/konveyor/move2kube-wasm/common" - "github.com/sirupsen/logrus" -) - -func copyDirToContainer(ctx context.Context, cli *client.Client, containerID, src, dst string) error { - reader := common.ReadFilesAsTar(src, dst, common.NoCompression) - if reader == nil { - err := fmt.Errorf("error during create tar archive from '%s'", src) - logrus.Error(err) - return err - } - defer reader.Close() - var clientErr, err error - doneChan := make(chan interface{}) - pr, pw := io.Pipe() - go func() { - clientErr = cli.CopyToContainer(ctx, containerID, "/", pr, types.CopyToContainerOptions{}) - close(doneChan) - }() - func() { - defer pw.Close() - var nBytesCopied int64 - nBytesCopied, err = io.Copy(pw, reader) - logrus.Debugf("%d bytes copied into pipe as tar", nBytesCopied) - }() - <-doneChan - if err == nil { - err = clientErr - } - return err -} - -func copyFromContainer(ctx context.Context, cli *client.Client, containerID string, containerPath, destPath string) (err error) { - content, stat, err := cli.CopyFromContainer(ctx, containerID, containerPath) - if err != nil { - return fmt.Errorf("failed to copy from the container with ID '%s' . Error: %w", containerID, err) - } - defer content.Close() - copyInfo := archive.CopyInfo{ - Path: containerPath, - Exists: true, - IsDir: stat.Mode.IsDir(), - } - preArchive := content - _, srcBase := archive.SplitPathDirEntry(copyInfo.Path) - preArchive = archive.RebaseArchiveEntries(content, srcBase, "") - return archive.CopyTo(preArchive, copyInfo, destPath) -} - -func copyDir(ctx context.Context, cli *client.Client, containerID, src, dst string) error { - reader := common.ReadFilesAsTar(src, dst, common.NoCompression) - if reader == nil { - err := fmt.Errorf("error during create tar archive from '%s'", src) - logrus.Error(err) - return err - } - defer reader.Close() - var clientErr, err error - doneChan := make(chan interface{}) - pr, pw := io.Pipe() - go func() { - clientErr = cli.CopyToContainer(ctx, containerID, "/", pr, types.CopyToContainerOptions{}) - close(doneChan) - }() - func() { - defer pw.Close() - var nBytesCopied int64 - nBytesCopied, err = io.Copy(pw, reader) - logrus.Debugf("%d bytes copied into pipe as tar", nBytesCopied) - }() - <-doneChan - if err == nil { - err = clientErr - } - return err -} +// +//import ( +// "context" +// "fmt" +// "io" +// +// "github.com/docker/docker/api/types" +// "github.com/docker/docker/client" +// "github.com/konveyor/move2kube-wasm/common" +// "github.com/sirupsen/logrus" +//) +// +//func copyDirToContainer(ctx context.Context, cli *client.Client, containerID, src, dst string) error { +// reader := common.ReadFilesAsTar(src, dst, common.NoCompression) +// if reader == nil { +// err := fmt.Errorf("error during create tar archive from '%s'", src) +// logrus.Error(err) +// return err +// } +// defer reader.Close() +// var clientErr, err error +// doneChan := make(chan interface{}) +// pr, pw := io.Pipe() +// go func() { +// clientErr = cli.CopyToContainer(ctx, containerID, "/", pr, types.CopyToContainerOptions{}) +// close(doneChan) +// }() +// func() { +// defer pw.Close() +// var nBytesCopied int64 +// nBytesCopied, err = io.Copy(pw, reader) +// logrus.Debugf("%d bytes copied into pipe as tar", nBytesCopied) +// }() +// <-doneChan +// if err == nil { +// err = clientErr +// } +// return err +//} +// +//func copyFromContainer(ctx context.Context, cli *client.Client, containerID string, containerPath, destPath string) (err error) { +// //content, stat, err := cli.CopyFromContainer(ctx, containerID, containerPath) +// //if err != nil { +// // return fmt.Errorf("failed to copy from the container with ID '%s' . Error: %w", containerID, err) +// //} +// //defer content.Close() +// //copyInfo := archive.CopyInfo{ +// // Path: containerPath, +// // Exists: true, +// // IsDir: stat.Mode.IsDir(), +// //} +// //preArchive := content +// //_, srcBase := archive.SplitPathDirEntry(copyInfo.Path) +// //preArchive = archive.RebaseArchiveEntries(content, srcBase, "") +// //return archive.CopyTo(preArchive, copyInfo, destPath) +// return nil +//} +// +//func copyDir(ctx context.Context, cli *client.Client, containerID, src, dst string) error { +// reader := common.ReadFilesAsTar(src, dst, common.NoCompression) +// if reader == nil { +// err := fmt.Errorf("error during create tar archive from '%s'", src) +// logrus.Error(err) +// return err +// } +// defer reader.Close() +// var clientErr, err error +// doneChan := make(chan interface{}) +// pr, pw := io.Pipe() +// go func() { +// clientErr = cli.CopyToContainer(ctx, containerID, "/", pr, types.CopyToContainerOptions{}) +// close(doneChan) +// }() +// func() { +// defer pw.Close() +// var nBytesCopied int64 +// nBytesCopied, err = io.Copy(pw, reader) +// logrus.Debugf("%d bytes copied into pipe as tar", nBytesCopied) +// }() +// <-doneChan +// if err == nil { +// err = clientErr +// } +// return err +//} diff --git a/go.mod b/go.mod index 90e34ca..9ae6f3b 100644 --- a/go.mod +++ b/go.mod @@ -6,11 +6,15 @@ go 1.21.0 require github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af require ( + github.com/Akash-Nayak/GopacheConfig v0.0.0-20210730101443-d5bfa3109be4 + github.com/BurntSushi/toml v1.0.0 github.com/Masterminds/semver/v3 v3.1.1 github.com/Masterminds/sprig v2.22.0+incompatible github.com/cloudfoundry-community/go-cfclient/v2 v2.0.0 github.com/docker/docker v24.0.0+incompatible github.com/gorilla/mux v1.8.0 + github.com/hashicorp/go-version v1.6.0 + github.com/magiconair/properties v1.8.7 github.com/mholt/archiver/v3 v3.5.1 github.com/mitchellh/mapstructure v1.5.0 github.com/pkg/errors v0.9.1 @@ -19,19 +23,19 @@ require ( github.com/spf13/viper v1.16.0 golang.org/x/crypto v0.12.0 golang.org/x/mod v0.10.0 + golang.org/x/text v0.12.0 gopkg.in/yaml.v3 v3.0.1 k8s.io/apimachinery v0.26.5 k8s.io/client-go v11.0.1-0.20190805182717-6502b5e7b1b5+incompatible ) require ( - github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver v1.5.0 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect github.com/Microsoft/hcsshim v0.11.2 // indirect github.com/andybalholm/brotli v1.0.3 // indirect - github.com/containerd/containerd v1.6.23 // indirect + github.com/containerd/containerd v1.7.5 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/distribution/reference v0.5.0 // indirect @@ -48,6 +52,7 @@ require ( github.com/google/gofuzz v1.2.0 // indirect github.com/google/uuid v1.3.0 // indirect github.com/googleapis/gnostic v0.5.5 // indirect + github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/huandu/xstrings v1.3.2 // indirect github.com/imdario/mergo v0.3.13 // indirect @@ -55,7 +60,6 @@ require ( github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.16.5 // indirect github.com/klauspost/pgzip v1.2.5 // indirect - github.com/magiconair/properties v1.8.7 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/moby/patternmatcher v0.6.0 // indirect @@ -71,6 +75,8 @@ require ( github.com/pelletier/go-toml/v2 v2.0.8 // indirect github.com/pierrec/lz4/v4 v4.1.2 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/smartystreets/assertions v1.1.0 // indirect + github.com/smartystreets/goconvey v1.6.4 // indirect github.com/spf13/afero v1.9.5 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect @@ -81,7 +87,6 @@ require ( golang.org/x/oauth2 v0.7.0 // indirect golang.org/x/sys v0.11.0 // indirect golang.org/x/term v0.11.0 // indirect - golang.org/x/text v0.12.0 // indirect golang.org/x/time v0.1.0 // indirect golang.org/x/tools v0.7.0 // indirect google.golang.org/appengine v1.6.7 // indirect @@ -91,7 +96,7 @@ require ( gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gotest.tools/v3 v3.5.1 // indirect - k8s.io/api v0.23.1 // indirect + k8s.io/api v0.26.2 // indirect k8s.io/klog/v2 v2.100.1 // indirect k8s.io/utils v0.0.0-20230505201702-9f6742963106 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect diff --git a/go.sum b/go.sum index ec44aff..e8dd5a0 100644 --- a/go.sum +++ b/go.sum @@ -41,6 +41,8 @@ cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3f dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Akash-Nayak/GopacheConfig v0.0.0-20210730101443-d5bfa3109be4 h1:h6Gvq//5x92mMX/CWrVNAOl7Oi4wdsRzhVgqKIMHDX8= +github.com/Akash-Nayak/GopacheConfig v0.0.0-20210730101443-d5bfa3109be4/go.mod h1:mioqvD+p/slck6WEOtQq+nWaV4Zw84fQjK4npdGRg7M= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= @@ -51,6 +53,8 @@ github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935 github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v1.0.0 h1:dtDWrepsVPfW9H/4y7dDgFc2MBUSeJhlaDtK13CxFlU= +github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= @@ -84,8 +88,8 @@ github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnht github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0 h1:sDMmm+q/3+BukdIpxwO365v/Rbspp2Nt5XntgQRXq8Q= github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM= -github.com/containerd/containerd v1.6.23 h1:KYJd6UJhKHzwMhiD70iTtSmU+k4565ac22GOTI3AuTA= -github.com/containerd/containerd v1.6.23/go.mod h1:UrQOiyzrLi3n4aezYJbQH6Il+YzTvnHFbEuO3yfDrM4= +github.com/containerd/containerd v1.7.5 h1:i9T9XpAWMe11BHMN7pu1BZqOGjXaKTPyz2v+KYOZgkY= +github.com/containerd/containerd v1.7.5/go.mod h1:ieJNCSzASw2shSGYLHx8NAE7WsZ/gEigo5fQ78W5Zvw= github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= @@ -222,12 +226,15 @@ github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2c github.com/googleapis/gnostic v0.5.5 h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw= github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 h1:l5lAOZEym3oK3SQ2HBHWsJUfbNBiTXJDeW2QDxw9AQ0= +github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= +github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= @@ -340,10 +347,12 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af h1:Sp5TG9f7K39yfB+If0vjp97vuT74F72r8hfRpP8jLU0= github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a h1:pa8hGb/2YqsZKovtsgrwcDH1RZhVbTKCjLp47XpqCDs= +github.com/smartystreets/assertions v1.1.0 h1:MkTeG1DMwsrdH7QtLXy5W+fUxWq+vmb6cLmyJ7aRtF0= +github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48= diff --git a/transformer/dockerfilegenerator/dotnet/constants.go b/transformer/dockerfilegenerator/dotnet/constants.go new file mode 100644 index 0000000..360f789 --- /dev/null +++ b/transformer/dockerfilegenerator/dotnet/constants.go @@ -0,0 +1,32 @@ +/* + * Copyright IBM Corporation 2022 + * + * 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 dotnet + +import ( + transformertypes "github.com/konveyor/move2kube-wasm/types/transformer" +) + +const ( + // CSPROJ_FILE_EXT is the file extension for C# (C Sharp) projects. + CSPROJ_FILE_EXT = ".csproj" + // LaunchSettingsJSON is the name of the json containing launch configuration + LaunchSettingsJSON = "launchSettings.json" + // DotNetCoreCsprojFilesPathType points to the csproj files path of dotnetcore projects + DotNetCoreCsprojFilesPathType transformertypes.PathType = "DotNetCoreCsprojPathType" + // DotNetCoreSolutionFilePathType points to the solution file path of a dot net core project + DotNetCoreSolutionFilePathType transformertypes.PathType = "DotNetCoreSolutionPathType" +) diff --git a/transformer/dockerfilegenerator/dotnet/utils.go b/transformer/dockerfilegenerator/dotnet/utils.go new file mode 100644 index 0000000..9c0e374 --- /dev/null +++ b/transformer/dockerfilegenerator/dotnet/utils.go @@ -0,0 +1,114 @@ +/* + * Copyright IBM Corporation 2022 + * + * 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 dotnet + +import ( + "encoding/xml" + "fmt" + "os" + "path/filepath" + "runtime" + "strings" + + //"github.com/konveyor/move2kube-wasm/qaengine" + "github.com/konveyor/move2kube-wasm/types/source/dotnet" +) + +type buildOption string + +const ( + // NO_BUILD_STAGE don't generate the build stage in Dockerfiles + NO_BUILD_STAGE buildOption = "no build stage" + // BUILD_IN_BASE_IMAGE generate the build stage and put it in a separate Dockerfile + BUILD_IN_BASE_IMAGE buildOption = "build stage in base image" + // BUILD_IN_EVERY_IMAGE generate the build stage in every Dockerfile + BUILD_IN_EVERY_IMAGE buildOption = "build stage in every image" +) + +// AskUserForDockerfileType asks the user what type of Dockerfiles to generate. +func AskUserForDockerfileType(rootProjectName string) (buildOption, error) { + //TODO: WASI + //quesId := common.JoinQASubKeys(common.ConfigServicesKey, `"`+rootProjectName+`"`, "dockerfileType") + //desc := fmt.Sprintf("What type of Dockerfiles should be generated for the service '%s'?", rootProjectName) + //options := []string{ + // string(NO_BUILD_STAGE), + // string(BUILD_IN_BASE_IMAGE), + // string(BUILD_IN_EVERY_IMAGE), + //} + def := BUILD_IN_BASE_IMAGE + //hints := []string{ + // fmt.Sprintf("[%s] There is no build stage. Dockerfiles will only contain the run stage. The .dll files will need to be built and present in the file system already, for them to get copied into the container.", NO_BUILD_STAGE), + // fmt.Sprintf("[%s] Put the build stage in a separate Dockerfile and create a base image.", BUILD_IN_BASE_IMAGE), + // fmt.Sprintf("[%s] Put the build stage in every Dockerfile to make it self contained. (Warning: This may cause one build per Dockerfile.)", BUILD_IN_EVERY_IMAGE), + //} + //selectedBuildOption := buildOption(qaengine.FetchSelectAnswer(quesId, desc, hints, string(def), options, nil)) + selectedBuildOption := NO_BUILD_STAGE + switch selectedBuildOption { + case NO_BUILD_STAGE, BUILD_IN_BASE_IMAGE, BUILD_IN_EVERY_IMAGE: + return selectedBuildOption, nil + } + return def, fmt.Errorf("user selected an unsupported option for generating Dockerfiles. Actual: %s", selectedBuildOption) +} + +// GetCSProjPathsFromSlnFile parses the solution file for cs project file paths. +// If "allPaths" is true then every path we find will be returned (not just c sharp project files). +func GetCSProjPathsFromSlnFile(inputPath string, allPaths bool) ([]string, error) { + slnBytes, err := os.ReadFile(inputPath) + if err != nil { + return nil, fmt.Errorf("failed to open the solution file at path %s . Error: %q", inputPath, err) + } + csProjPaths := []string{} + subMatches := dotnet.ProjBlockRegex.FindAllStringSubmatch(string(slnBytes), -1) + notWindows := runtime.GOOS != "windows" + for _, subMatch := range subMatches { + if len(subMatch) == 0 { + continue + } + csProjPath := strings.Trim(subMatch[1], `"`) + if notWindows { + csProjPath = strings.ReplaceAll(csProjPath, `\`, string(os.PathSeparator)) + } + if !allPaths && filepath.Ext(csProjPath) != CSPROJ_FILE_EXT { + continue + } + csProjPaths = append(csProjPaths, csProjPath) + } + return csProjPaths, nil +} + +// ParseCSProj parses a c sharp project file +func ParseCSProj(path string) (dotnet.CSProj, error) { + configuration := dotnet.CSProj{} + csProjBytes, err := os.ReadFile(path) + if err != nil { + return configuration, fmt.Errorf("failed to read the c sharp project file at path %s . Error: %q", path, err) + } + if err := xml.Unmarshal(csProjBytes, &configuration); err != nil { + return configuration, fmt.Errorf("failed to parse the c sharp project file at path %s . Error: %q", path, err) + } + return configuration, nil +} + +// GetChildProjectName gets the child project name give the path to the c sharp project file +func GetChildProjectName(csProjPath string) string { + return strings.TrimSuffix(filepath.Base(csProjPath), filepath.Ext(csProjPath)) +} + +// GetParentProjectName gets the parent project name give the path to the visual studio solution file +func GetParentProjectName(slnPath string) string { + return strings.TrimSuffix(filepath.Base(slnPath), filepath.Ext(slnPath)) +} diff --git a/transformer/dockerfilegenerator/dotnetcoredockerfilegenerator.go b/transformer/dockerfilegenerator/dotnetcoredockerfilegenerator.go new file mode 100644 index 0000000..4d2266e --- /dev/null +++ b/transformer/dockerfilegenerator/dotnetcoredockerfilegenerator.go @@ -0,0 +1,711 @@ +/* + * Copyright IBM Corporation 2021 + * + * 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 dockerfilegenerator + +import ( + "encoding/xml" + "fmt" + "net/url" + "path/filepath" + "regexp" + "strings" + + "github.com/konveyor/move2kube-wasm/common" + "github.com/konveyor/move2kube-wasm/environment" + //"github.com/konveyor/move2kube-wasm/qaengine" + dotnetutils "github.com/konveyor/move2kube-wasm/transformer/dockerfilegenerator/dotnet" + //irtypes "github.com/konveyor/move2kube-wasm/types/ir" + //"github.com/konveyor/move2kube-wasm/types/qaengine/commonqa" + "github.com/konveyor/move2kube-wasm/types/source/dotnet" + transformertypes "github.com/konveyor/move2kube-wasm/types/transformer" + "github.com/konveyor/move2kube-wasm/types/transformer/artifacts" + "github.com/sirupsen/logrus" + "github.com/spf13/cast" +) + +const ( + buildStageC = "dotnetcorebuildstage" + defaultBuildOutputDir = "bin/Release" + defaultBuildOutputSubDir = "publish" + defaultDotNetCoreVersion = "6.0" + defaultDotNetCoreFramework = "net" + defaultDotNetCoreVersion +) + +var ( + dotnetcoreRegex = regexp.MustCompile(`net(?:(?:coreapp)|(?:standard))?(\d+\.\d+)`) +) + +// DotNetCoreTemplateConfig implements DotNetCore config interface +type DotNetCoreTemplateConfig struct { + IncludeBuildStage bool + BuildStageImageTag string + BuildContainerName string + IsNodeJSProject bool + PublishProfilePath string + IncludeRunStage bool + RunStageImageTag string + Ports []int32 + EntryPointPath string + CopyFrom string + EnvVariables map[string]string + NodeVersion string + NodeVersionProperties map[string]string + PackageManager string +} + +// ----------------------------------------------------------------------------------- +// C Sharp Project XML file +// ----------------------------------------------------------------------------------- + +// PublishProfile defines the publish profile +type PublishProfile struct { + XMLName xml.Name `xml:"Project"` + PropertyGroup *PropertyGroup `xml:"PropertyGroup"` +} + +// PropertyGroup has publish properties of the project +type PropertyGroup struct { + XMLName xml.Name `xml:"PropertyGroup"` + PublishUrl string `xml:"PublishUrl"` + PublishUrlS string `xml:"publishUrl"` +} + +// ----------------------------------------------------------------------------------- +// Visual Studio launch settings json file +// ----------------------------------------------------------------------------------- + +// LaunchSettings is to load the launchSettings.json properties +type LaunchSettings struct { + Profiles map[string]LaunchProfile `json:"profiles"` +} + +// LaunchProfile implements launch profile properties +type LaunchProfile struct { + CommandName string `json:"CommandName"` + LaunchBrowser bool `json:"launchBrowser"` + ApplicationURL string `json:"applicationUrl"` + EnvironmentVariables map[string]string `json:"environmentVariables"` +} + +// ----------------------------------------------------------------------------------- +// Transformer +// ----------------------------------------------------------------------------------- + +// DotNetCoreDockerfileGenerator implements the Transformer interface +type DotNetCoreDockerfileGenerator struct { + Config transformertypes.Transformer + Env *environment.Environment + DotNetCoreConfig *DotNetCoreDockerfileYamlConfig + Spec NodeVersionsMappingSpec + SortedVersions []string +} + +// DotNetCoreDockerfileYamlConfig represents the configuration of the DotNetCore dockerfile +type DotNetCoreDockerfileYamlConfig struct { + DefaultDotNetCoreVersion string `yaml:"defaultDotNetCoreVersion"` + NodejsDockerfileYamlConfig +} + +// Init Initializes the transformer +func (t *DotNetCoreDockerfileGenerator) Init(tc transformertypes.Transformer, env *environment.Environment) error { + t.Config = tc + t.Env = env + + // load the config + t.DotNetCoreConfig = &DotNetCoreDockerfileYamlConfig{} + if err := common.GetObjFromInterface(t.Config.Spec.Config, t.DotNetCoreConfig); err != nil { + return fmt.Errorf("failed to load the config for the transformer %+v into %T . Error: %q", t.Config.Spec.Config, t.DotNetCoreConfig, err) + } + if t.DotNetCoreConfig.DefaultDotNetCoreVersion == "" { + t.DotNetCoreConfig.DefaultDotNetCoreVersion = defaultDotNetCoreVersion + } + if t.DotNetCoreConfig.DefaultPackageManager == "" { + t.DotNetCoreConfig.DefaultPackageManager = defaultPackageManager + } + + // load the version mapping file + mappingFilePath := filepath.Join(t.Env.GetEnvironmentContext(), versionMappingFilePath) + spec, err := LoadNodeVersionMappingsFile(mappingFilePath) + if err != nil { + return fmt.Errorf("failed to load the node version mappings file at path %s . Error: %q", versionMappingFilePath, err) + } + t.Spec = spec + if t.DotNetCoreConfig.DefaultNodejsVersion == "" { + t.DotNetCoreConfig.DefaultNodejsVersion = t.Spec.NodeVersions[0][versionKey] + } + return nil +} + +// GetConfig returns the transformer config +func (t *DotNetCoreDockerfileGenerator) GetConfig() (transformertypes.Transformer, *environment.Environment) { + return t.Config, t.Env +} + +// DirectoryDetect runs detect in each sub directory +func (t *DotNetCoreDockerfileGenerator) DirectoryDetect(dir string) (services map[string][]transformertypes.Artifact, err error) { + slnPaths, err := common.GetFilesByExtInCurrDir(dir, []string{dotnet.VISUAL_STUDIO_SOLUTION_FILE_EXT}) + if err != nil { + return nil, fmt.Errorf("failed to list the dot net visual studio solution files in the directory %s . Error: %q", dir, err) + } + if len(slnPaths) == 0 { + return nil, nil + } + if len(slnPaths) > 1 { + logrus.Debugf("more than one visual studio solution file detected. Number of .sln files %d", len(slnPaths)) + } + slnPath := slnPaths[0] + appName := dotnetutils.GetParentProjectName(slnPath) + normalizedAppName := common.MakeStringK8sServiceNameCompliant(appName) + relCSProjPaths, err := dotnetutils.GetCSProjPathsFromSlnFile(slnPath, false) + if err != nil { + return nil, fmt.Errorf("failed to parse the vs solution file at path %s . Error: %q", slnPath, err) + } + if len(relCSProjPaths) == 0 { + return nil, fmt.Errorf("no child projects found in the solution file at the path: %s", slnPath) + } + childProjects := []artifacts.DotNetChildProject{} + csProjPaths := []string{} + + for _, relCSProjPath := range relCSProjPaths { + csProjPath := filepath.Join(dir, relCSProjPath) + configuration, err := dotnetutils.ParseCSProj(csProjPath) + if err != nil { + logrus.Errorf("failed to parse the c sharp project file at path %s . Error: %q", csProjPath, err) + continue + } + idx := common.FindIndex(configuration.PropertyGroups, func(x dotnet.PropertyGroup) bool { return x.TargetFramework != "" }) + if idx == -1 { + logrus.Debugf("failed to find the target framework in any of the property groups inside the c sharp project file at path %s", csProjPath) + continue + } + targetFramework := configuration.PropertyGroups[idx].TargetFramework + if !dotnetcoreRegex.MatchString(targetFramework) { + logrus.Errorf("dot net core tranformer: the c sharp project file at path %s does not have a supported asp.net framework version. Actual version: %s", csProjPath, targetFramework) + continue + } + isAspNet, err := isAspNet(configuration) + if err != nil { + logrus.Errorf("failed to determine if the c sharp project file at the path '%s' is an ASP NET web project. Error: %q", csProjPath, err) + continue + } + if !isAspNet { + logrus.Debugf("the c sharp project file at path %s is not an asp net web project. skipping.", csProjPath) + continue + } + // foundNonSilverLightWebProject = true + childProjectName := dotnetutils.GetChildProjectName(csProjPath) + normalizedChildProjectName := common.MakeStringK8sServiceNameCompliant(childProjectName) + csProjPaths = append(csProjPaths, csProjPath) + childProjects = append(childProjects, artifacts.DotNetChildProject{ + Name: normalizedChildProjectName, + OriginalName: childProjectName, + RelCSProjPath: relCSProjPath, + TargetFramework: targetFramework, + }) + } + if len(csProjPaths) == 0 { + return nil, nil + } + namedServices := map[string][]transformertypes.Artifact{ + normalizedAppName: {{ + Paths: map[transformertypes.PathType][]string{ + artifacts.ServiceRootDirPathType: {dir}, + artifacts.ServiceDirPathType: {dir}, + dotnetutils.DotNetCoreSolutionFilePathType: {slnPath}, + dotnetutils.DotNetCoreCsprojFilesPathType: csProjPaths, + }, + Configs: map[transformertypes.ConfigType]interface{}{ + artifacts.DotNetConfigType: artifacts.DotNetConfig{ + IsDotNetCore: true, + DotNetAppName: appName, + IsSolutionFilePresent: true, + ChildProjects: childProjects, + }, + }, + }}, + } + return namedServices, nil +} + +// Transform transforms the artifacts +func (t *DotNetCoreDockerfileGenerator) Transform(newArtifacts []transformertypes.Artifact, oldArtifacts []transformertypes.Artifact) ([]transformertypes.PathMapping, []transformertypes.Artifact, error) { + pathMappings := []transformertypes.PathMapping{} + artifactsCreated := []transformertypes.Artifact{} + for _, newArtifact := range newArtifacts { + dotNetConfig := artifacts.DotNetConfig{} + if err := newArtifact.GetConfig(artifacts.DotNetConfigType, &dotNetConfig); err != nil || !dotNetConfig.IsDotNetCore { + continue + } + if len(newArtifact.Paths[artifacts.ServiceDirPathType]) == 0 || len(newArtifact.Paths[dotnetutils.DotNetCoreCsprojFilesPathType]) == 0 { + logrus.Errorf("the service directory is missing from the dot net core artifact: %+v", newArtifact) + continue + } + t1, t2, err := t.TransformArtifact(newArtifact, oldArtifacts, dotNetConfig) + if err != nil { + logrus.Errorf("failed to trasnform the dot net core artifact: %+v . Error: %q", newArtifact, err) + continue + } + pathMappings = append(pathMappings, t1...) + artifactsCreated = append(artifactsCreated, t2...) + } + return pathMappings, artifactsCreated, nil +} + +// TransformArtifact transforms a single artifact +func (t *DotNetCoreDockerfileGenerator) TransformArtifact(newArtifact transformertypes.Artifact, oldArtifacts []transformertypes.Artifact, dotNetConfig artifacts.DotNetConfig) ([]transformertypes.PathMapping, []transformertypes.Artifact, error) { + pathMappings := []transformertypes.PathMapping{} + artifactsCreated := []transformertypes.Artifact{} + + selectedBuildOption, err := dotnetutils.AskUserForDockerfileType(newArtifact.Name) + if err != nil { + return pathMappings, artifactsCreated, fmt.Errorf("failed to ask the user what type of dockerfile they prefer. Error: %q", err) + } + logrus.Debugf("user chose to generate Dockefiles that have '%s'", selectedBuildOption) + + // ask the user which child projects should be run in the K8s cluster + + selectedChildProjectNames := []string{} + for _, childProject := range dotNetConfig.ChildProjects { + selectedChildProjectNames = append(selectedChildProjectNames, childProject.Name) + } + if len(selectedChildProjectNames) > 1 { + //TODO: WASI + //quesKey := fmt.Sprintf(common.ConfigServicesDotNetChildProjectsNamesKey, `"`+newArtifact.Name+`"`) + //desc := fmt.Sprintf("For the multi-project Dot Net Core app '%s', please select all the child projects that should be run as services in the cluster:", newArtifact.Name) + //hints := []string{"deselect any child project that should not be run (example: libraries)"} + //selectedChildProjectNames = qaengine.FetchMultiSelectAnswer(quesKey, desc, hints, selectedChildProjectNames, selectedChildProjectNames, nil) + if len(selectedChildProjectNames) == 0 { + return pathMappings, artifactsCreated, fmt.Errorf("user deselected all the child projects of the dot net core multi-project app '%s'", newArtifact.Name) + } + } + + serviceDir := newArtifact.Paths[artifacts.ServiceRootDirPathType][0] + relServiceDir, err := filepath.Rel(t.Env.GetEnvironmentSource(), serviceDir) + if err != nil { + return pathMappings, artifactsCreated, fmt.Errorf("failed to make the service directory path %s relative to the source directory %s . Error: %q", serviceDir, t.Env.GetEnvironmentSource(), err) + } + //TODO: WASI + //ir := irtypes.IR{} + //irPresent := true + //if err := newArtifact.GetConfig(irtypes.IRConfigType, &ir); err != nil { + // irPresent = false + // logrus.Debugf("failed to load the IR config from the dot net artifact. Error: %q Artifact: %+v", err, newArtifact) + //} + // + //detectedPorts := ir.GetAllServicePorts() + detectedPorts := []int32{} + + // copy over the source dir to hold the dockerfiles we genrate + + pathMappings = append(pathMappings, transformertypes.PathMapping{ + Type: transformertypes.SourcePathMappingType, + DestPath: common.DefaultSourceDir, + }) + + // build is always done at the top level using the .sln file regardless of the build option selected + + imageToCopyFrom := common.MakeStringContainerImageNameCompliant(newArtifact.Name + "-" + buildStageC) + if selectedBuildOption == dotnetutils.NO_BUILD_STAGE { + imageToCopyFrom = "" // files will be copied from the local file system instead of a builder image + } + + // look for a package.json file to see if the project requires nodejs installed in order to build + + isNodeJSProject := false + nodeVersion := t.DotNetCoreConfig.DefaultNodejsVersion + packageManager := t.DotNetCoreConfig.DefaultPackageManager + packageJsonPaths, err := common.GetFilesByName(serviceDir, []string{packageJSONFile}, nil) + if err != nil { + return pathMappings, artifactsCreated, fmt.Errorf("failed to look for package.json files in the directory %s . Error: %q", serviceDir, err) + } + if len(packageJsonPaths) > 0 { + versionConstraints := []string{} + packageManagers := []string{} + for _, packageJsonPath := range packageJsonPaths { + packageJson := PackageJSON{} + if err := common.ReadJSON(packageJsonPath, &packageJson); err != nil { + logrus.Errorf("failed to parse the package.json file at path %s . Error: %q", packageJsonPath, err) + continue + } + isNodeJSProject = true + if versionConstraint, ok := packageJson.Engines["node"]; ok { + versionConstraints = append(versionConstraints, versionConstraint) + } + if packageJson.PackageManager != "" { + parts := strings.Split(packageJson.PackageManager, "@") + if len(parts) > 0 { + packageManagers = append(packageManagers, parts[0]) + } + } + } + if len(versionConstraints) > 0 { + nodeVersion = getNodeVersion(versionConstraints[0], t.DotNetCoreConfig.DefaultNodejsVersion, t.SortedVersions) + } + if len(packageManagers) > 0 { + packageManager = packageManagers[0] + } + } + + // generate the base image Dockerfile + var props map[string]string + if idx := common.FindIndex(t.Spec.NodeVersions, func(x map[string]string) bool { return x[versionKey] == nodeVersion }); idx != -1 { + props = t.Spec.NodeVersions[idx] + } + if selectedBuildOption == dotnetutils.BUILD_IN_BASE_IMAGE { + webConfig := DotNetCoreTemplateConfig{ + IncludeBuildStage: true, + BuildStageImageTag: defaultDotNetCoreVersion, + BuildContainerName: imageToCopyFrom, + IsNodeJSProject: isNodeJSProject, + NodeVersion: nodeVersion, + NodeVersionProperties: props, + PackageManager: packageManager, + } + + // path mapping to generate the Dockerfile for the child project + + dockerfilePath := filepath.Join(common.DefaultSourceDir, relServiceDir, common.DefaultDockerfileName+"."+buildStageC) + pathMappings = append(pathMappings, transformertypes.PathMapping{ + Type: transformertypes.TemplatePathMappingType, + SrcPath: common.DefaultDockerfileName, + DestPath: dockerfilePath, + TemplateConfig: webConfig, + }) + + // artifacts to inform other transformers of the Dockerfile we generated + + paths := map[transformertypes.PathType][]string{artifacts.DockerfilePathType: {dockerfilePath}} + serviceName := artifacts.ServiceConfig{ServiceName: newArtifact.Name} + imageName := artifacts.ImageName{ImageName: imageToCopyFrom} + dockerfileArtifact := transformertypes.Artifact{ + Name: imageName.ImageName, + Type: artifacts.DockerfileArtifactType, + Paths: paths, + Configs: map[transformertypes.ConfigType]interface{}{ + artifacts.ServiceConfigType: serviceName, + artifacts.ImageNameConfigType: imageName, + }, + } + artifactsCreated = append(artifactsCreated, dockerfileArtifact) + } + + for _, childProject := range dotNetConfig.ChildProjects { + + // only look at the child modules the user selected + + if !common.IsPresent(selectedChildProjectNames, childProject.Name) { + logrus.Debugf("skipping the child project '%s' because it wasn't selected", childProject.Name) + continue + } + + // parse the .csproj file to get the build output path + + csProjPath := filepath.Join(serviceDir, childProject.RelCSProjPath) + configuration, err := dotnetutils.ParseCSProj(csProjPath) + if err != nil { + logrus.Errorf("failed to parse the c sharp project file at path %s . Error: %q", csProjPath, err) + continue + } + + // data to fill the Dockerfile template + + relCSProjDir := filepath.Dir(childProject.RelCSProjPath) + targetFramework := defaultDotNetCoreFramework + targetFrameworkVersion := "" + if idx := common.FindIndex(configuration.PropertyGroups, func(x dotnet.PropertyGroup) bool { return x.TargetFramework != "" }); idx != -1 { + targetFramework = configuration.PropertyGroups[idx].TargetFramework + version := dotnetcoreRegex.FindAllStringSubmatch(targetFramework, -1) + if len(version) == 0 || len(version[0]) != 2 { + logrus.Warnf("unable to find compatible version for the '%s' service. Using default version: %s . Actual: %+v", newArtifact.Name, t.DotNetCoreConfig.DefaultDotNetCoreVersion, configuration.PropertyGroups) + targetFrameworkVersion = t.DotNetCoreConfig.DefaultDotNetCoreVersion + } else { + targetFrameworkVersion = version[0][1] + } + } + buildOutputDir := defaultBuildOutputDir + if idx := common.FindIndex(configuration.PropertyGroups, func(x dotnet.PropertyGroup) bool { return x.OutputPath != "" }); idx != -1 { + buildOutputDir = filepath.Clean(common.GetUnixPath(configuration.PropertyGroups[idx].OutputPath)) + } + relBuildOutputDir := filepath.Join(buildOutputDir, targetFramework, defaultBuildOutputSubDir) + copyFrom := filepath.Join("/src", relCSProjDir, relBuildOutputDir) + if selectedBuildOption == dotnetutils.BUILD_IN_EVERY_IMAGE { + copyFrom = filepath.Join("/src", relBuildOutputDir) // only the files for the child project are in the builder image + } else if selectedBuildOption == dotnetutils.NO_BUILD_STAGE { + copyFrom = relBuildOutputDir // files will be copied from the local file system instead of a builder image + } + + // find all the publish profile files for this child project + + childProjectDir := filepath.Join(serviceDir, relCSProjDir) + publishProfilePaths, err := common.GetFilesByExt(childProjectDir, []string{".pubxml"}) + if err != nil { + logrus.Errorf("failed to look for asp net publish profile (.pubxml) files in the directory %s . Error: %q", childProjectDir, err) + continue + } + + // select a profile to use for publishing the child project + + qaSubKey := common.JoinQASubKeys(`"`+newArtifact.Name+`"`, "childProjects", `"`+childProject.Name+`"`) + relSelectedProfilePath, _, err := getPublishProfile(publishProfilePaths, qaSubKey, serviceDir) + if err != nil { + logrus.Errorf("failed to select one of the publish profiles for the asp net app. Error: %q Profiles: %+v", err, publishProfilePaths) + continue + } + + nodeVersion := t.DotNetCoreConfig.DefaultNodejsVersion + var props map[string]string + if idx := common.FindIndex(t.Spec.NodeVersions, func(x map[string]string) bool { return x[versionKey] == nodeVersion }); idx != -1 { + props = t.Spec.NodeVersions[idx] + } + + templateConfig := DotNetCoreTemplateConfig{ + IncludeBuildStage: selectedBuildOption == dotnetutils.BUILD_IN_EVERY_IMAGE, + BuildStageImageTag: defaultDotNetCoreVersion, + BuildContainerName: imageToCopyFrom, + EntryPointPath: childProject.OriginalName + ".dll", + RunStageImageTag: targetFrameworkVersion, + IncludeRunStage: true, + CopyFrom: common.GetUnixPath(copyFrom), + PublishProfilePath: common.GetUnixPath(relSelectedProfilePath), + EnvVariables: map[string]string{}, + NodeVersion: nodeVersion, + NodeVersionProperties: props, + PackageManager: t.DotNetCoreConfig.DefaultPackageManager, + } + + // look for a package.json file to see if the project requires nodejs installed in order to build + + if isNodeJSProject { + childPackageJsonPaths := common.Filter(packageJsonPaths, func(x string) bool { return common.IsParent(x, childProjectDir) }) + if len(childPackageJsonPaths) > 0 { + if len(childPackageJsonPaths) > 1 { + logrus.Warnf("found multiple package.json files for the child project %s . Actual: %+v", childProject.Name, childPackageJsonPaths) + } + packageJsonPath := childPackageJsonPaths[0] + packageJson := PackageJSON{} + if err := common.ReadJSON(packageJsonPath, &packageJson); err != nil { + logrus.Errorf("failed to parse the package.json file at path %s . Error: %q", packageJsonPath, err) + } else { + templateConfig.IsNodeJSProject = true + if nodeVersionConstraint, ok := packageJson.Engines["node"]; ok { + nodeVersion = getNodeVersion(nodeVersionConstraint, t.DotNetCoreConfig.DefaultNodejsVersion, t.SortedVersions) + var props map[string]string + if idx := common.FindIndex(t.Spec.NodeVersions, func(x map[string]string) bool { return x[versionKey] == nodeVersion }); idx != -1 { + props = t.Spec.NodeVersions[idx] + } + templateConfig.NodeVersion = nodeVersion + templateConfig.NodeVersionProperties = props + } + if packageJson.PackageManager != "" { + parts := strings.Split(packageJson.PackageManager, "@") + if len(parts) > 0 { + templateConfig.PackageManager = parts[0] + } + } + } + } + } + + // look for a launchSettings.json file to get port numbers the app listens on + + launchJsonPaths, err := common.GetFilesByName(childProjectDir, []string{dotnetutils.LaunchSettingsJSON}, nil) + if err != nil { + logrus.Errorf("failed to look for launchSettings.json files in the directory %s . Error: %q", childProjectDir, err) + continue + } + + childProjectPorts := append([]int32{}, detectedPorts...) + if len(launchJsonPaths) > 0 { + if len(launchJsonPaths) > 1 { + logrus.Warnf("found multiple launchSettings.json files. Actual: %+v", launchJsonPaths) + } + launchJsonPath := launchJsonPaths[0] + launchSettings := LaunchSettings{} + if err := common.ReadJSON(launchJsonPath, &launchSettings); err != nil { + logrus.Errorf("failed to parse the launchSettings.json file at path %s . Error: %q", launchJsonPath, err) + continue + } + if v, ok := launchSettings.Profiles[childProject.Name]; ok && v.ApplicationURL != "" { + + // extract ports and set environment variables + + newAppUrls, ports, err := modifyUrlsToListenOnAllAddresses(v.ApplicationURL) + if err != nil { + logrus.Errorf("failed to parse and modify the application listen urls: '%s' . Error: %q", v.ApplicationURL, err) + continue + } + templateConfig.EnvVariables["ASPNETCORE_URLS"] = newAppUrls + for _, port := range ports { + childProjectPorts = common.AppendIfNotPresent(childProjectPorts, port) + } + } + } + if len(childProjectPorts) == 0 { + childProjectPorts = append(childProjectPorts, common.DefaultServicePort) + } + + // have the user select the ports to use for the child project + + //templateConfig.Ports = commonqa.GetPortsForService(childProjectPorts, qaSubKey) + + dockerfilePath := filepath.Join(common.DefaultSourceDir, relServiceDir, relCSProjDir, common.DefaultDockerfileName) + pathMappings = append(pathMappings, transformertypes.PathMapping{ + Type: transformertypes.TemplatePathMappingType, + SrcPath: common.DefaultDockerfileName, + DestPath: dockerfilePath, + TemplateConfig: templateConfig, + }) + + // artifacts to inform other transformers of the Dockerfile we generated + + paths := map[transformertypes.PathType][]string{artifacts.DockerfilePathType: {dockerfilePath}} + serviceConfig := artifacts.ServiceConfig{ServiceName: childProject.Name} + imageName := artifacts.ImageName{ImageName: common.MakeStringContainerImageNameCompliant(childProject.Name)} + dockerfileArtifact := transformertypes.Artifact{ + Name: imageName.ImageName, + Type: artifacts.DockerfileArtifactType, + Paths: paths, + Configs: map[transformertypes.ConfigType]interface{}{ + artifacts.ServiceConfigType: serviceConfig, + artifacts.ImageNameConfigType: imageName, + }, + } + dockerfileServiceArtifact := transformertypes.Artifact{ + Name: imageName.ImageName, + Type: artifacts.DockerfileForServiceArtifactType, + Paths: paths, + Configs: map[transformertypes.ConfigType]interface{}{ + artifacts.ServiceConfigType: serviceConfig, + artifacts.ImageNameConfigType: imageName, + }, + } + //TODO: WASI + //if irPresent { + // dockerfileServiceArtifact.Configs[irtypes.IRConfigType] = ir + //} + artifactsCreated = append(artifactsCreated, dockerfileArtifact, dockerfileServiceArtifact) + } + + return pathMappings, artifactsCreated, nil +} + +// utility functions + +// isAspNet checks if the given app is an ASP NET web app +func isAspNet(configuration dotnet.CSProj) (bool, error) { + if len(configuration.ItemGroups) == 0 { + if strings.HasSuffix(configuration.Sdk, ".NET.Sdk.Web") { + return true, nil + } + logrus.Debugf("cannot determine if it's an ASP NET project, the .csproj file does not have any item groups") + return false, nil + } + for _, itemGroup := range configuration.ItemGroups { + if len(itemGroup.References) > 0 { + for _, r := range itemGroup.References { + if dotnet.AspNetWebLib.MatchString(r.Include) { + return true, nil + } + } + } + if len(itemGroup.PackageReferences) > 0 { + for _, r := range itemGroup.PackageReferences { + if dotnet.AspNetWebLib.MatchString(r.Include) { + return true, nil + } + } + } + } + return false, nil +} + +// getPublishProfile asks the user to select one of the publish profiles for the child project +func getPublishProfile(profilePaths []string, subKey, baseDir string) (string, string, error) { + if len(profilePaths) == 0 { + return "", "", nil + } + relProfilePaths := []string{} + for _, profilePath := range profilePaths { + relProfilePath, err := filepath.Rel(baseDir, profilePath) + if err != nil { + return "", "", fmt.Errorf("failed to make the path %s relative to the directory %s . Error: %q", profilePath, baseDir, err) + } + relProfilePaths = append(relProfilePaths, relProfilePath) + } + relSelectedProfilePath := relProfilePaths[0] + if len(relProfilePaths) > 1 { + //TODO: WASI + //quesKey := common.JoinQASubKeys(common.ConfigServicesKey, subKey, common.ConfigPublishProfileForServiceKeySegment) + //desc := fmt.Sprintf("Select the profile to be use for publishing the ASP.NET child project %s :", subKey) + //relSelectedProfilePath = qaengine.FetchSelectAnswer(quesKey, desc, nil, relSelectedProfilePath, relProfilePaths, nil) + } + selectedProfilePath := filepath.Join(baseDir, relSelectedProfilePath) + publishUrl, err := parsePublishProfileFile(selectedProfilePath) + if err != nil { + return relSelectedProfilePath, "", fmt.Errorf("failed to parse the publish profile file at path %s . Error: %q", selectedProfilePath, err) + } + return relSelectedProfilePath, publishUrl, nil +} + +// parsePublishProfileFile parses the publish profile to get the PublishUrl +func parsePublishProfileFile(profilePath string) (string, error) { + publishProfile := PublishProfile{} + if err := common.ReadXML(profilePath, &publishProfile); err != nil { + return "", fmt.Errorf("failed to read publish profile file at path %s as xml. Error: %q", profilePath, err) + } + if publishProfile.PropertyGroup == nil { + return "", nil + } + if publishProfile.PropertyGroup.PublishUrl != "" { + return common.GetUnixPath(publishProfile.PropertyGroup.PublishUrl), nil + } + if publishProfile.PropertyGroup.PublishUrlS != "" { + return common.GetUnixPath(publishProfile.PropertyGroup.PublishUrlS), nil + } + return "", nil +} + +func modifyUrlsToListenOnAllAddresses(src string) (string, []int32, error) { + srcs := strings.Split(src, ";") + newUrls := []string{} + ports := []int32{} + for _, s := range srcs { + u, err := url.Parse(s) + if err != nil { + return "", nil, fmt.Errorf("failed to parse the string '%s' as a url. Error: %q", s, err) + } + if u.Scheme == "https" { + u.Scheme = "http" + } + if u.Scheme != "http" { + continue + } + parts := strings.Split(u.Host, ":") + if len(parts) != 2 { + return "", nil, fmt.Errorf("expected there to be a host and port separated by a colon. Actual: %#v", u) + } + u.Host = "*:" + parts[1] + newUrls = append(newUrls, u.String()) + if len(parts[1]) == 0 { + ports = append(ports, 80) + } + if port, err := cast.ToInt32E(parts[1]); err == nil { + ports = append(ports, port) + } + } + return strings.Join(newUrls, ";"), ports, nil +} diff --git a/transformer/dockerfilegenerator/golangdockerfiletransformer.go b/transformer/dockerfilegenerator/golangdockerfiletransformer.go index f53e577..dc7e358 100644 --- a/transformer/dockerfilegenerator/golangdockerfiletransformer.go +++ b/transformer/dockerfilegenerator/golangdockerfiletransformer.go @@ -42,8 +42,6 @@ const ( GolangModFilePathType transformertypes.PathType = "GoModFilePath" // GolangVersionsMappingKind defines kind of GolangVersionMappingKind GolangVersionsMappingKind types.Kind = "GolangVersionsMapping" - imageTagKey = "imageTag" - versionKey = "version" ) // GolangVersionsMapping stores the Go versions mapping diff --git a/transformer/dockerfilegenerator/java/earanalyser.go b/transformer/dockerfilegenerator/java/earanalyser.go new file mode 100644 index 0000000..f7e6104 --- /dev/null +++ b/transformer/dockerfilegenerator/java/earanalyser.go @@ -0,0 +1,118 @@ +/* + * Copyright IBM Corporation 2021 + * + * 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 java + +import ( + "fmt" + "path/filepath" + "strings" + + "github.com/konveyor/move2kube-wasm/common" + "github.com/konveyor/move2kube-wasm/environment" + transformertypes "github.com/konveyor/move2kube-wasm/types/transformer" + "github.com/konveyor/move2kube-wasm/types/transformer/artifacts" + "github.com/sirupsen/logrus" +) + +// EarAnalyser implements Transformer interface +type EarAnalyser struct { + Config transformertypes.Transformer + Env *environment.Environment + EarConfig *EarYamlConfig +} + +// EarYamlConfig stores the war related information +type EarYamlConfig struct { + JavaVersion string `yaml:"defaultJavaVersion"` +} + +// EarDockerfileTemplate stores parameters for the dockerfile template +type EarDockerfileTemplate struct { + DeploymentFile string + BuildContainerName string + DeploymentFileDirInBuildContainer string + EnvVariables map[string]string +} + +// Init Initializes the transformer +func (t *EarAnalyser) Init(tc transformertypes.Transformer, env *environment.Environment) (err error) { + t.Config = tc + t.Env = env + t.EarConfig = &EarYamlConfig{} + err = common.GetObjFromInterface(t.Config.Spec.Config, t.EarConfig) + if err != nil { + logrus.Errorf("unable to load config for Transformer %+v into %T : %s", t.Config.Spec.Config, t.EarConfig, err) + return err + } + if t.EarConfig.JavaVersion == "" { + t.EarConfig.JavaVersion = defaultJavaVersion + } + return nil +} + +// GetConfig returns the transformer config +func (t *EarAnalyser) GetConfig() (transformertypes.Transformer, *environment.Environment) { + return t.Config, t.Env +} + +// DirectoryDetect runs detect in each sub directory +func (t *EarAnalyser) DirectoryDetect(dir string) (map[string][]transformertypes.Artifact, error) { + paths, err := common.GetFilesInCurrentDirectory(dir, nil, []string{".*[.]ear"}) + if err != nil { + return nil, fmt.Errorf("failed to look for .ear archives in the directory %s . Error: %q", dir, err) + } + if len(paths) == 0 { + return nil, nil + } + services := map[string][]transformertypes.Artifact{} + for _, path := range paths { + relPath, err := filepath.Rel(t.Env.GetEnvironmentSource(), path) + if err != nil { + logrus.Errorf("failed to make the path %s relative to the source code directory %s . Error: %q", path, t.Env.GetEnvironmentSource(), err) + continue + } + serviceName := strings.TrimSuffix(filepath.Base(path), filepath.Ext(path)) + normalizedServiceName := common.MakeStringK8sServiceNameCompliant(serviceName) + newArtifact := transformertypes.Artifact{ + Paths: map[transformertypes.PathType][]string{ + artifacts.EarPathType: {path}, + artifacts.ServiceDirPathType: {filepath.Dir(path)}, + }, + Configs: map[transformertypes.ConfigType]interface{}{ + artifacts.EarConfigType: artifacts.EarArtifactConfig{ + DeploymentFilePath: relPath, + JavaVersion: t.EarConfig.JavaVersion, + }, + artifacts.OriginalNameConfigType: artifacts.OriginalNameConfig{OriginalName: serviceName}, + artifacts.ImageNameConfigType: artifacts.ImageName{ImageName: common.MakeStringContainerImageNameCompliant(serviceName)}, + }, + } + services[normalizedServiceName] = append(services[normalizedServiceName], newArtifact) + } + return services, nil +} + +// Transform transforms the artifacts +func (t *EarAnalyser) Transform(newArtifacts []transformertypes.Artifact, alreadySeenArtifacts []transformertypes.Artifact) ([]transformertypes.PathMapping, []transformertypes.Artifact, error) { + pathMappings := []transformertypes.PathMapping{} + createdArtifacts := []transformertypes.Artifact{} + for _, a := range newArtifacts { + a.Type = artifacts.EarArtifactType + createdArtifacts = append(createdArtifacts, a) + } + return pathMappings, createdArtifacts, nil +} diff --git a/transformer/dockerfilegenerator/java/gradle/constants.go b/transformer/dockerfilegenerator/java/gradle/constants.go new file mode 100644 index 0000000..430e17d --- /dev/null +++ b/transformer/dockerfilegenerator/java/gradle/constants.go @@ -0,0 +1,79 @@ +/* + * Copyright IBM Corporation 2021 + * + * 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 gradle + +import ( + "regexp" +) + +const ( + repositoriesProp = "repositories" + dependenciesProp = "dependencies" + pluginsProp = "plugins" +) + +const ( + charTab = '\t' + charNewLine = '\n' + charCarriageReturn = '\r' + charSpace = ' ' + charLeftParanthesis = '(' + charRightParanthesis = ')' + charPeriod = '.' + charSlash = '/' + charEquals = '=' + charArrayStart = '[' + charArrayEnd = ']' + charBlockStart = '{' + charBlockEnd = '}' + + keywordDef = "def" + keywordIf = "if" + + singleLineCommentStart = `//` + blockCommentStart = `/*` + blockCommentEnd = `*/` +) + +var ( + depsKeywordString = `[ \t]*([A-Za-z0-9_-]+)[ \t]*` + depsKeywordStringRegex = regexp.MustCompile(depsKeywordString) + depsHardGavStringRegex = regexp.MustCompile(depsKeywordString + `(?:\((.*)\)|(.*))`) + depsExcludeLineRegex = regexp.MustCompile(`exclude[ \t]+([^\n]+)`) + + projectRegex = regexp.MustCompile(`(project\([^\)]+\))`) + functionRegex = regexp.MustCompile(`\w+\(.*\);?$`) + + whitespacecharacters = map[rune]bool{ + charTab: true, + charNewLine: true, + charCarriageReturn: true, + charSpace: true, + } + + depsEasyGavStringPattern = `("?)([\w.-]+):([\w.-]+):([\w\[\]\(\),+.-]+)("?)` + depsItemBlockPattern = depsKeywordString + `\((("?)(.*)("?))\)[ \t]*\{` + pluginsLinePattern = `(id|version)(?:[ \t]*)("?)([A-Za-z0-9-.]+)("?)` + + depsEasyGavStringRegex, depsItemBlockRegex, pluginsLineRegex quotedRegex +) + +func init() { + depsEasyGavStringRegex.Init(depsEasyGavStringPattern) + depsItemBlockRegex.Init(depsItemBlockPattern) + pluginsLineRegex.Init(pluginsLinePattern) +} diff --git a/transformer/dockerfilegenerator/java/gradle/gradleparser.go b/transformer/dockerfilegenerator/java/gradle/gradleparser.go new file mode 100644 index 0000000..5ebb4be --- /dev/null +++ b/transformer/dockerfilegenerator/java/gradle/gradleparser.go @@ -0,0 +1,599 @@ +/* + * Copyright IBM Corporation 2021 + * + * 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 gradle + +import ( + "os" + "strings" + + "github.com/sirupsen/logrus" +) + +// Inspired from https://github.com/ninetwozero/gradle-to-js/blob/master/lib/parser.js + +// ParseGardleBuildFile parses a gradle build file +func ParseGardleBuildFile(buildFilePath string) (gradleBuild Gradle, err error) { + state := gradleParseState{} + buildFile, err := os.ReadFile(buildFilePath) + if err != nil { + logrus.Errorf("Unable to read gradle build file : %s", err) + return Gradle{}, err + } + return deepParse(string(buildFile), &state, false, true), nil +} + +func deepParse(chunk string, state *gradleParseState, keepFunctionCalls, skipEmptyValues bool) (parsedGradleOutput Gradle) { + parsedGradleOutput = Gradle{} + var character rune + var tempString, commentText string + var currentKey string + parsingKey := true + isBeginningOfLine := true + + for chunkLength := len([]rune(chunk)); state.index < chunkLength; state.index++ { + character = ([]rune(chunk))[state.index] + if isBeginningOfLine && isWhitespace(character) { + continue + } + + if !state.comment.parsing && isBeginningOfLine && isStartOfComment(tempString) { + isBeginningOfLine = false + if isSingleLineComment(tempString) { + state.comment.setSingleLine() + } else { + state.comment.setMultiLine() + } + continue + } + + if state.comment.multiLine && isEndOfMultiLineComment(commentText) { + state.comment.reset() + isBeginningOfLine = true + tempString = "" + commentText = "" + continue + } + + if state.comment.parsing && character != charNewLine { + commentText += string(character) + continue + } + + if state.comment.parsing && isLineBreakCharacter(character) { + if state.comment.singleLine { + state.comment.reset() + isBeginningOfLine = true + + currentKey = "" + tempString = "" + commentText = "" + } + continue + } + + if parsingKey && !keepFunctionCalls && character == charLeftParanthesis { + skipFunctionCall(chunk, state) + currentKey = "" + tempString = "" + isBeginningOfLine = true + continue + } + if isLineBreakCharacter(character) { + if currentKey == "" && tempString != "" { + if parsingKey { + if isFunctionCall(tempString) && !keepFunctionCalls { + continue + } else { + currentKey = strings.TrimSpace(tempString) + tempString = "" + } + } + } + if tempString != "" || (currentKey != "" && !skipEmptyValues) { + addValueToStructure(&parsedGradleOutput, currentKey, trimWrappingQuotes(tempString)) + currentKey = "" + tempString = "" + } + parsingKey = true + isBeginningOfLine = true + + state.comment.reset() + continue + } + // Only parse as an array if the first *real* char is a [ + if !parsingKey && tempString == "" && character == charArrayStart { + addValueToStructure(&parsedGradleOutput, currentKey, parseArray(chunk, state)...) + currentKey = "" + tempString = "" + continue + } + if character == charBlockStart { + // We need to skip the current (=start) character so that we literally "step into" the next closure/block + state.index++ + switch currentKey { + case repositoriesProp: + parsedGradleOutput.Repositories = append(parsedGradleOutput.Repositories, parseRepositoryClosure(chunk, state)...) + case dependenciesProp: + parsedGradleOutput.Dependencies = append(parsedGradleOutput.Dependencies, parseDependencyClosure(chunk, state)...) + case pluginsProp: + parsedGradleOutput.Plugins = append(parsedGradleOutput.Plugins, parsePluginsClosure(chunk, state)...) + default: + if parsedGradleOutput.Blocks == nil { + parsedGradleOutput.Blocks = map[string]Gradle{} + } + if _, ok := parsedGradleOutput.Blocks[currentKey]; ok { + gb := parsedGradleOutput.Blocks[currentKey] + gb.Merge(deepParse(chunk, state, keepFunctionCalls, skipEmptyValues)) + parsedGradleOutput.Blocks[currentKey] = gb + } else { + parsedGradleOutput.Blocks[currentKey] = deepParse(chunk, state, keepFunctionCalls, skipEmptyValues) + } + } + currentKey = "" + } else if character == charBlockEnd { + currentKey = "" + tempString = "" + break + } else if isDelimiter(character) && parsingKey { + if isKeyword(tempString) { + if tempString == keywordDef { + tempString = fetchDefinedNameOrSkipFunctionDefinition(chunk, state) + } else if tempString == keywordIf { + skipIfStatement(chunk, state) + currentKey = "" + tempString = "" + continue + } + } + currentKey = tempString + tempString = "" + parsingKey = false + if currentKey == "" { + continue + } + } else { + if tempString == "" && isDelimiter(character) { + continue + } + tempString += string(character) + isBeginningOfLine = isBeginningOfLine && (character == charSlash || isStartOfComment(tempString)) + } + } + // Add the last value to the structure + addValueToStructure(&parsedGradleOutput, currentKey, trimWrappingQuotes(tempString)) + return parsedGradleOutput +} + +func skipIfStatement(chunk string, state *gradleParseState) bool { + skipFunctionCall(chunk, state) + chunkAsRune := []rune(chunk) + var character rune + var hasFoundTheCurlyBraces, hasFoundAStatementWithoutBraces bool + curlyBraceCount := 0 + for max := len(chunkAsRune); state.index < max; state.index++ { + character = chunkAsRune[state.index] + if hasFoundAStatementWithoutBraces { + if isLineBreakCharacter(character) { + break + } + } else { + if character == charBlockStart { + hasFoundTheCurlyBraces = true + curlyBraceCount++ + } else if character == charBlockEnd { + curlyBraceCount-- + } else if !hasFoundTheCurlyBraces && !isWhitespace(character) { + hasFoundAStatementWithoutBraces = true + } + if hasFoundTheCurlyBraces && curlyBraceCount == 0 { + break + } + } + } + return curlyBraceCount == 0 +} + +func skipFunctionDefinition(chunk string, state *gradleParseState) { + parenthesisNest := 1 + state.index++ + chunkAsRuneA := []rune(chunk) + chunkLen := len(chunkAsRuneA) + var character rune + for chunkLen < state.index && parenthesisNest != 0 { + character = chunkAsRuneA[state.index] + if character == charLeftParanthesis { + parenthesisNest++ + } else if character == charRightParanthesis { + parenthesisNest-- + } + state.index++ + character = chunkAsRuneA[state.index] + } + for chunkLen < state.index && character != charBlockStart { + state.index++ + character = chunkAsRuneA[state.index] + } + state.index++ + character = chunkAsRuneA[state.index] + blockNest := 1 + for chunkLen < state.index && blockNest != 0 { + if character == charBlockStart { + blockNest++ + } else if character == charBlockEnd { + blockNest-- + } + state.index++ + character = chunkAsRuneA[state.index] + } + state.index-- +} + +func parseDependencyClosure(chunk string, state *gradleParseState) []GradleDependency { + specialClosures := parseSpecialClosure(chunk, state) + gradleDependencies := []GradleDependency{} + for _, specialClosure := range specialClosures { + gradleDependencies = append(gradleDependencies, createStructureForDependencyItem(specialClosure)) + } + return gradleDependencies +} + +func createStructureForDependencyItem(data string) GradleDependency { + gdi := GradleDependency{} + if match := depsItemBlockRegex.FindStringSubmatch(data); len(match) > 2 { + excludes := []map[string]string{} + excludeMatches := depsExcludeLineRegex.FindAllStringSubmatch(data, -1) + for _, excludeMatch := range excludeMatches { + excludes = append(excludes, parseMapNotation(excludeMatch[0][findFirstSpaceOrTabPosition(excludeMatch[0]):])) + } + gdi.GradleGAV = parseGavString(match[2]) + gdi.Type = match[1] + gdi.Excludes = excludes + } else { + gdi.GradleGAV = parseGavString(data) + parsed := depsKeywordStringRegex.FindStringSubmatch(data) + if len(parsed) > 1 { + gdi.Type = parsed[1] + } + } + return gdi +} + +func parsePluginsClosure(chunk string, state *gradleParseState) []GradlePlugin { + specialClosures := parseSpecialClosure(chunk, state) + gradlePlugins := []GradlePlugin{} + for _, specialClosure := range specialClosures { + gradlePlugins = append(gradlePlugins, createStructureForPlugin(specialClosure)) + } + return gradlePlugins +} + +func createStructureForPlugin(pluginRow string) map[string]string { + plugin := map[string]string{} + matches := pluginsLineRegex.FindAllStringSubmatch(pluginRow) + for _, match := range matches { + if len(match) > 1 { + plugin[match[1]] = match[3] + } + } + return plugin +} + +func findFirstSpaceOrTabPosition(input string) int { + position := strings.Index(input, " ") + if position == -1 { + position = strings.Index(input, "\t") + } + return position +} + +func parseGavString(gavString string) (gav GradleGAV) { + gav = GradleGAV{} + easyGavStringMatches := depsEasyGavStringRegex.FindStringSubmatch(gavString) + if len(easyGavStringMatches) > 3 { + gav.Group = easyGavStringMatches[2] + gav.Name = easyGavStringMatches[3] + gav.Version = easyGavStringMatches[4] + } else if strings.Contains(gavString, `project(`) { + gav.Name = projectRegex.FindString(gavString) + } else { + hardGavMatches := depsHardGavStringRegex.FindStringSubmatch(gavString) + if len(hardGavMatches) > 2 { + if hardGavMatches[3] != "" { + gav = parseMapNotationWithFallback(gav, hardGavMatches[3], "") + } else { + gav = parseMapNotationWithFallback(gav, hardGavMatches[2], "") + } + } else { + gav = parseMapNotationWithFallback(gav, gavString, gavString[findFirstSpaceOrTabPosition(gavString):]) + } + } + return gav +} + +func parseMapNotationWithFallback(gav GradleGAV, str, name string) GradleGAV { + parsedMap := parseMapNotation(str) + if _, ok := parsedMap["name"]; ok { + return GradleGAV{ + Group: parsedMap["group"], + Name: parsedMap["name"], + Version: parsedMap["version"], + } + } + if name == "" { + name = str + } + gav.Name = name + return gav +} + +func parseMapNotation(input string) (parsedMap map[string]string) { + parsedMap = map[string]string{} + currentKey := "" + var quotation rune + inputAsRune := []rune(input) + for i, max := 0, len(inputAsRune); i < max; i++ { + if inputAsRune[i] == ':' { + currentKey = strings.TrimSpace(currentKey) + parsedMap[currentKey] = "" + var innerLoop rune + for i = i + 1; i < max; i++ { + if innerLoop == 0 { + // Skip any leading spaces before the actual value + if isWhitespace(inputAsRune[i]) { + continue + } + } + // We just take note of what the "latest" quote was so that we can + if inputAsRune[i] == '"' || inputAsRune[i] == '\'' { + quotation = inputAsRune[i] + continue + } + // Moving on to the next value if we find a comma + if inputAsRune[i] == ',' { + parsedMap[currentKey] = strings.TrimSpace(parsedMap[currentKey]) + currentKey = "" + break + } + parsedMap[currentKey] += string(inputAsRune[i]) + innerLoop++ + } + } else { + currentKey += string(inputAsRune[i]) + } + } + // If the last character contains a quotation mark, we remove it + if val, ok := parsedMap[currentKey]; ok { + parsedMap[currentKey] = strings.TrimSuffix(strings.TrimSpace(val), string(quotation)) + } + return parsedMap +} + +func parseRepositoryClosure(chunk string, state *gradleParseState) (repositories []GradleRepository) { + repositories = []GradleRepository{} + parsedRepos := deepParse(chunk, state, true, false) + for parsedRepoType, value := range parsedRepos.Metadata { + if len(value) > 0 && value[0] != "" { + repositories = append(repositories, GradleRepository{Type: parsedRepoType, Data: GradleRepositoryData{ + Name: value[0], + }}) + } else { + repositories = append(repositories, GradleRepository{Type: "unknown", Data: GradleRepositoryData{Name: parsedRepoType}}) + } + } + return repositories +} + +func parseSpecialClosure(chunk string, state *gradleParseState) (closures []string) { + closures = []string{} + // openBlockCount starts at 1 due to us entering after " {" + openBlockCount := 1 + currentKey := "" + currentValue := "" + chunkAsRune := []rune(chunk) + + isInItemBlock := false + for ; state.index < len(chunkAsRune); state.index++ { + if chunkAsRune[state.index] == charBlockStart { + openBlockCount++ + } else if chunkAsRune[state.index] == charBlockEnd { + openBlockCount-- + } else { + currentKey += string(chunkAsRune[state.index]) + } + + // Keys shouldn't have any leading nor trailing whitespace + currentKey = strings.TrimSpace(currentKey) + + if isStartOfComment(currentKey) { + var commentText = currentKey + for state.index++; state.index < len(chunkAsRune); state.index++ { + if isCommentComplete(commentText, chunkAsRune[state.index]) { + currentKey = "" + break + } + commentText += string(chunkAsRune[state.index]) + } + } + + if currentKey != "" && isWhitespace(chunkAsRune[state.index]) { + var character rune + for state.index++; state.index < len(chunkAsRune); state.index++ { + character = chunkAsRune[state.index] + currentValue += string(character) + if character == charBlockStart { + isInItemBlock = true + } else if isInItemBlock && character == charBlockEnd { + isInItemBlock = false + } else if !isInItemBlock { + if isLineBreakCharacter(character) && currentValue != "" { + break + } + } + } + + closures = append(closures, currentKey+" "+currentValue) + currentKey = "" + currentValue = "" + } + + if openBlockCount == 0 { + break + } + } + return closures +} + +func fetchDefinedNameOrSkipFunctionDefinition(chunk string, state *gradleParseState) string { + var character rune + checkAsRune := []rune(chunk) + temp := "" + isVariableDefinition := true + for max := len(checkAsRune); state.index < max; state.index++ { + character = checkAsRune[state.index] + + if character == charEquals { + // Variable definition, break and return name + break + } else if character == charLeftParanthesis { + // Function definition, skip parsing + isVariableDefinition = false + skipFunctionDefinition(chunk, state) + break + } + temp += string(character) + } + + if isVariableDefinition { + values := strings.Split(strings.TrimSpace(temp), " ") + return values[len(values)-1] + } + return "" +} + +func parseArray(chunk string, state *gradleParseState) []string { + var character rune + chunkAsRune := []rune(chunk) + temp := "" + for max := len(chunkAsRune); state.index < max; state.index++ { + character = chunkAsRune[state.index] + if character == charArrayStart { + continue + } else if character == charArrayEnd { + break + } + temp += string(character) + } + elems := strings.Split(temp, ",") + for elemI, elem := range elems { + elems[elemI] = trimWrappingQuotes(strings.TrimSpace(elem)) + } + return elems +} + +func skipFunctionCall(chunk string, state *gradleParseState) bool { + openParenthesisCount := 0 + checkAsRune := []rune(chunk) + var character rune + for max := len(checkAsRune); state.index < max; state.index++ { + character = checkAsRune[state.index] + if character == charLeftParanthesis { + openParenthesisCount++ + } else if character == charRightParanthesis { + openParenthesisCount-- + } + if openParenthesisCount == 0 && !isWhitespace(character) { + state.index++ + break + } + } + return openParenthesisCount == 0 +} + +func addValueToStructure(gradleBuild *Gradle, currentKey string, value ...string) { + switch currentKey { + case "": + return + case repositoriesProp: + fallthrough + case dependenciesProp: + fallthrough + case pluginsProp: + logrus.Errorf("Incompatible value while parsing for %s", currentKey) + default: + if gradleBuild.Metadata == nil { + gradleBuild.Metadata = map[string][]string{} + } + gradleBuild.Metadata[currentKey] = append(gradleBuild.Metadata[currentKey], value...) + } +} + +func trimWrappingQuotes(str string) string { + doubleQuote := `"` + singleQuote := "'" + str = strings.TrimSpace(str) + if strings.HasPrefix(str, doubleQuote) { + str = strings.TrimPrefix(strings.TrimSuffix(str, doubleQuote), doubleQuote) + } else if strings.HasPrefix(str, singleQuote) { + str = strings.TrimPrefix(strings.TrimSuffix(str, singleQuote), singleQuote) + } + return str +} + +func isDelimiter(character rune) bool { + return character == charSpace || character == charEquals +} + +func isWhitespace(character rune) bool { + return whitespacecharacters[character] +} + +func isLineBreakCharacter(character rune) bool { + return character == charCarriageReturn || character == charNewLine +} + +func isKeyword(str string) bool { + return str == keywordDef || str == keywordIf +} + +func isSingleLineComment(comment string) bool { + return comment[0:2] == singleLineCommentStart +} + +func isStartOfComment(snippet string) bool { + return snippet == blockCommentStart || snippet == singleLineCommentStart +} + +func isCommentComplete(text string, next rune) bool { + return (isLineBreakCharacter(next) && isSingleLineComment(text)) || (isWhitespace(next) && isEndOfMultiLineComment(text)) +} + +func isEndOfMultiLineComment(comment string) bool { + if len(comment) < 2 { + return false + } + return comment[len(comment)-2:] == blockCommentEnd +} + +func isFunctionCall(str string) bool { + return functionRegex.MatchString(str) +} + +// GetSingleArgumentFromFuntionCall returns the single argument in a function call +func GetSingleArgumentFromFuntionCall(str string, functionName string) string { + str = trimWrappingQuotes(strings.TrimSpace(strings.Trim(strings.Trim(strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(str), functionName)), "("), ")"))) + return str +} diff --git a/transformer/dockerfilegenerator/java/gradle/quotedRegex.go b/transformer/dockerfilegenerator/java/gradle/quotedRegex.go new file mode 100644 index 0000000..cd3d030 --- /dev/null +++ b/transformer/dockerfilegenerator/java/gradle/quotedRegex.go @@ -0,0 +1,61 @@ +/* + * Copyright IBM Corporation 2021 + * + * 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 gradle + +import ( + "regexp" + "strings" +) + +type quotedRegex struct { + regexes []regexp.Regexp +} + +// Init initializes the regexes +func (q *quotedRegex) Init(regex string) { + q.regexes = []regexp.Regexp{} + q.regexes = append(q.regexes, *regexp.MustCompile(regex)) + q.regexes = append(q.regexes, *regexp.MustCompile(strings.ReplaceAll(regex, `"`, "'"))) +} + +// FindStringSubmatch mimics regexp's FindStringSubmatch +func (q *quotedRegex) FindStringSubmatch(str string) []string { + var regex regexp.Regexp + matchIndex := -1 + for _, r := range q.regexes { + match := r.FindStringSubmatchIndex(str) + if len(match) > 0 { + if matchIndex == -1 || matchIndex > match[0] { + matchIndex = match[0] + regex = r + } + } + } + if matchIndex != -1 { + return regex.FindStringSubmatch(str) + } + return nil +} + +// FindAllStringSubmatch mimics regexp's FindAllStringSubmatch +func (q *quotedRegex) FindAllStringSubmatch(str string) (matches [][]string) { + matches = [][]string{} + for _, r := range q.regexes { + matches = append(matches, r.FindAllStringSubmatch(str, -1)...) + } + return matches +} diff --git a/transformer/dockerfilegenerator/java/gradle/types.go b/transformer/dockerfilegenerator/java/gradle/types.go new file mode 100644 index 0000000..00e3b09 --- /dev/null +++ b/transformer/dockerfilegenerator/java/gradle/types.go @@ -0,0 +1,136 @@ +/* + * Copyright IBM Corporation 2021 + * + * 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 gradle + +import ( + "strings" + + "github.com/konveyor/move2kube-wasm/common" +) + +const ( + pluginPrefix = "plugin:" +) + +// Gradle stores parsed gradle +type Gradle struct { + Repositories []GradleRepository `yaml:"repositories,omitempty" json:"repositories,omitempty"` + Dependencies []GradleDependency `yaml:"dependencies,omitempty" json:"dependencies,omitempty"` + Plugins []GradlePlugin `yaml:"plugins,omitempty" json:"plugins,omitempty"` + Metadata map[string][]string `yaml:"metadata,omitempty" json:"metadata,omitempty"` + Blocks map[string]Gradle `yaml:"blocks,omitempty" json:"blocks,omitempty"` +} + +// GetPluginIDs returns plugins +func (g *Gradle) GetPluginIDs() (plugins []string) { + for _, p := range g.Plugins { + if val, ok := p["id"]; ok { + plugins = append(plugins, val) + } + } + for mk, mv := range g.Metadata { + if mk != "apply" { + continue + } + for _, v := range mv { + if strings.HasPrefix(v, pluginPrefix) { + pluginValue := common.StripQuotes(strings.TrimSpace(strings.TrimPrefix(v, pluginPrefix))) + plugins = append(plugins, pluginValue) + } + } + } + return plugins +} + +// Merge merges two parsed gradles +func (g *Gradle) Merge(newg Gradle) { + g.Repositories = append(g.Repositories, newg.Repositories...) + g.Dependencies = append(g.Dependencies, newg.Dependencies...) + g.Plugins = append(g.Plugins, newg.Plugins...) + if g.Metadata == nil { + g.Metadata = make(map[string][]string) + } + if g.Blocks == nil { + g.Blocks = make(map[string]Gradle) + } + for mi, m := range newg.Metadata { + g.Metadata[mi] = append(g.Metadata[mi], m...) + } + for bi, b := range newg.Blocks { + if ob, ok := g.Blocks[bi]; ok { + ob.Merge(b) + g.Blocks[bi] = ob + } else { + g.Blocks[bi] = b + } + } +} + +// GradleGAV stores GAV +type GradleGAV struct { + Group string `yaml:"group" json:"group"` + Name string `yaml:"name" json:"name"` + Version string `yaml:"version" json:"version"` +} + +// GradleDependency stores dependency +type GradleDependency struct { + GradleGAV + Type string `yaml:"type" json:"type"` + Excludes []map[string]string `yaml:"excludes,omitempty" json:"excludes,omitempty"` +} + +// GradleRepository stores repository +type GradleRepository struct { + Type string `yaml:"type" json:"type"` + Data GradleRepositoryData `yaml:"data,omitempty" json:"data,omitempty"` +} + +// GradleRepositoryData stores respository data +type GradleRepositoryData struct { + Name string `yaml:"name" json:"name"` +} + +// GradlePlugin stores repository plugin +type GradlePlugin map[string]string + +type gradleParseState struct { + index int + comment gradleComment +} + +type gradleComment struct { + parsing, singleLine, multiLine bool +} + +func (g *gradleComment) setSingleLine() { + g.setCommentState(true, false) +} + +func (g *gradleComment) setMultiLine() { + g.setCommentState(false, true) +} + +func (g *gradleComment) reset() { + g.setCommentState(false, false) +} + +func (g *gradleComment) setCommentState(singleLine, multiLine bool) { + g.singleLine = singleLine + g.multiLine = multiLine + g.parsing = singleLine || multiLine +} diff --git a/transformer/dockerfilegenerator/java/gradleanalyser.go b/transformer/dockerfilegenerator/java/gradleanalyser.go new file mode 100644 index 0000000..9fed5ee --- /dev/null +++ b/transformer/dockerfilegenerator/java/gradleanalyser.go @@ -0,0 +1,1020 @@ +/* + * Copyright IBM Corporation 2021 + * + * 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 java + +import ( + "bufio" + "fmt" + "os" + "path/filepath" + "regexp" + "strings" + + "github.com/konveyor/move2kube-wasm/common" + "github.com/konveyor/move2kube-wasm/environment" + //"github.com/konveyor/move2kube-wasm/qaengine" + "github.com/konveyor/move2kube-wasm/transformer/dockerfilegenerator/java/gradle" + //"github.com/konveyor/move2kube-wasm/types/qaengine/commonqa" + transformertypes "github.com/konveyor/move2kube-wasm/types/transformer" + "github.com/konveyor/move2kube-wasm/types/transformer/artifacts" + "github.com/magiconair/properties" + "github.com/sirupsen/logrus" + "github.com/spf13/cast" +) + +// An example parsed build.gradle file (useful for writing more gradle parsing logic) +// --------------------------------------------------------------------------------------------------------- +// var exampleBuildGradleFile = &gradle.Gradle{ +// Repositories: []gradle.GradleRepository{ +// { +// Type: "unknown", +// Data: gradle.GradleRepositoryData{Name: "mavenCentral()"}, +// }, +// }, +// Dependencies: []gradle.GradleDependency{ +// gradle.GradleDependency{ +// GradleGAV: gradle.GradleGAV{ +// Group: "", +// Name: "\"org.springframework.boot:spring-boot-starter-web\"", +// Version: "", +// }, +// Type: "implementation", +// Excludes: []map[string]string(nil), +// }, +// gradle.GradleDependency{ +// GradleGAV: gradle.GradleGAV{ +// Group: "", +// Name: "\"org.springframework.boot:spring-boot-starter-actuator\"", +// Version: "", +// }, +// Type: "implementation", +// Excludes: []map[string]string(nil), +// }, +// gradle.GradleDependency{ +// GradleGAV: gradle.GradleGAV{ +// Group: "", +// Name: "\"org.springframework.boot:spring-boot-starter-data-jpa\"", +// Version: "", +// }, +// Type: "implementation", +// Excludes: []map[string]string(nil), +// }, +// gradle.GradleDependency{ +// GradleGAV: gradle.GradleGAV{ +// Group: "", +// Name: "\"org.springframework.boot:spring-boot-starter-data-mongodb\"", +// Version: "", +// }, +// Type: "implementation", +// Excludes: []map[string]string(nil), +// }, +// gradle.GradleDependency{ +// GradleGAV: gradle.GradleGAV{ +// Group: "", +// Name: "\"org.springframework.boot:spring-boot-starter-data-redis\"", +// Version: "", +// }, +// Type: "implementation", +// Excludes: []map[string]string(nil), +// }, +// gradle.GradleDependency{ +// GradleGAV: gradle.GradleGAV{ +// Group: "", +// Name: "\"org.springframework.boot:spring-boot-starter-validation\"", +// Version: "", +// }, +// Type: "implementation", +// Excludes: []map[string]string(nil), +// }, +// gradle.GradleDependency{GradleGAV: gradle.GradleGAV{Group: "", Name: "\"io.pivotal.cfenv:java-cfenv-boot:${javaCfEnvVersion}\"", Version: ""}, Type: "implementation", Excludes: []map[string]string(nil)}, +// gradle.GradleDependency{GradleGAV: gradle.GradleGAV{Group: "", Name: "\"org.apache.commons:commons-pool2\"", Version: ""}, Type: "runtimeOnly", Excludes: []map[string]string(nil)}, +// gradle.GradleDependency{GradleGAV: gradle.GradleGAV{Group: "", Name: "\"com.h2database:h2\"", Version: ""}, Type: "runtimeOnly", Excludes: []map[string]string(nil)}, +// gradle.GradleDependency{GradleGAV: gradle.GradleGAV{Group: "", Name: "\"mysql:mysql-connector-java\"", Version: ""}, Type: "runtimeOnly", Excludes: []map[string]string(nil)}, +// gradle.GradleDependency{GradleGAV: gradle.GradleGAV{Group: "", Name: "\"org.postgresql:postgresql\"", Version: ""}, Type: "runtimeOnly", Excludes: []map[string]string(nil)}, +// gradle.GradleDependency{GradleGAV: gradle.GradleGAV{Group: "", Name: "\"com.microsoft.sqlserver:mssql-jdbc\"", Version: ""}, Type: "runtimeOnly", Excludes: []map[string]string(nil)}, +// gradle.GradleDependency{GradleGAV: gradle.GradleGAV{Group: "org.webjars", Name: "bootstrap", Version: "3.1.1"}, Type: "implementation", Excludes: []map[string]string(nil)}, +// gradle.GradleDependency{GradleGAV: gradle.GradleGAV{Group: "org.webjars", Name: "angularjs", Version: "1.2.16"}, Type: "implementation", Excludes: []map[string]string(nil)}, +// gradle.GradleDependency{GradleGAV: gradle.GradleGAV{Group: "org.webjars", Name: "angular-ui", Version: "0.4.0-2"}, Type: "implementation", Excludes: []map[string]string(nil)}, +// gradle.GradleDependency{GradleGAV: gradle.GradleGAV{Group: "org.webjars", Name: "angular-ui-bootstrap", Version: "0.10.0-1"}, Type: "implementation", Excludes: []map[string]string(nil)}, +// gradle.GradleDependency{GradleGAV: gradle.GradleGAV{Group: "org.webjars", Name: "jquery", Version: "2.1.0-2"}, Type: "implementation", Excludes: []map[string]string(nil)}, +// gradle.GradleDependency{GradleGAV: gradle.GradleGAV{Group: "", Name: "\"junit:junit\"", Version: ""}, Type: "testImplementation", Excludes: []map[string]string(nil)}, +// gradle.GradleDependency{ +// GradleGAV: gradle.GradleGAV{Group: "", Name: "\"org.springframework.boot:spring-boot-starter-test\"", Version: ""}, +// Type: "testImplementation", Excludes: []map[string]string(nil), +// }, +// }, +// Plugins: []gradle.GradlePlugin{ +// gradle.GradlePlugin{"id": "org.springframework.boot", "version": "2.6.7"}, +// gradle.GradlePlugin{"id": "io.spring.dependency-management", "version": "1.0.11.RELEASE"}, +// gradle.GradlePlugin{"id": "java"}, +// gradle.GradlePlugin{"id": "eclipse-wtp"}, +// gradle.GradlePlugin{"id": "idea"}, +// }, +// Metadata: map[string][]string(nil), +// Blocks: map[string]gradle.Gradle{ +// "ext": gradle.Gradle{Repositories: []gradle.GradleRepository(nil), Dependencies: []gradle.GradleDependency(nil), Plugins: []gradle.GradlePlugin(nil), Metadata: map[string][]string{"javaCfEnvVersion": []string{"2.4.0"}}, Blocks: map[string]gradle.Gradle(nil)}, +// "jar": gradle.Gradle{Repositories: []gradle.GradleRepository(nil), Dependencies: []gradle.GradleDependency(nil), Plugins: []gradle.GradlePlugin(nil), Metadata: map[string][]string{"enabled": []string{"false"}}, Blocks: map[string]gradle.Gradle(nil)}, +// "java": gradle.Gradle{ +// Repositories: []gradle.GradleRepository(nil), +// Dependencies: []gradle.GradleDependency(nil), +// Plugins: []gradle.GradlePlugin(nil), +// Metadata: map[string][]string{"sourceCompatibility": []string{"JavaVersion.VERSION_1_8"}, "targetCompatibility": []string{"JavaVersion.VERSION_1_8"}}, +// Blocks: map[string]gradle.Gradle(nil), +// }, +// }, +// } + +// --------------------------------------------------------------- + +// GradleAnalyser implements Transformer interface +type GradleAnalyser struct { + Config transformertypes.Transformer + Env *environment.Environment + GradleConfig *GradleYamlConfig +} + +// GradleYamlConfig stores the Gradle related information +type GradleYamlConfig struct { + GradleVersion string `yaml:"defaultGradleVersion"` + JavaVersion string `yaml:"defaultJavaVersion"` + AppPathInBuildContainer string `yaml:"appPathInBuildContainer"` +} + +// GradleBuildDockerfileTemplate defines the information for the build dockerfile template +type GradleBuildDockerfileTemplate struct { + GradlewPresent bool + JavaPackageName string + GradleVersion string + BuildContainerName string + GradleProperties map[string]string + EnvVariables map[string]string +} + +type gradleInfoT struct { + Name string + Type transformertypes.ArtifactType + IsParentBuild bool + IsGradlewPresent bool + JavaVersion string + DeploymentFilePath string + GradleProperties map[string]string + ChildModules []artifacts.GradleChildModule + SpringBoot *artifacts.SpringBootConfig +} + +const ( + gradleBuildFileName = "build.gradle" + gradleSettingsFileName = "settings.gradle" + archiveFileC = "archiveFile" + archiveFileNameC = "archiveFileName" + archiveBaseNameC = "archiveBaseName" + archiveAppendixC = "archiveAppendix" + archiveClassifierC = "archiveClassifier" + archiveVersionC = "archiveVersion" + archiveExtensionC = "archiveExtension" + archiveNameC = "archiveName" + archiveBaseNameOldC = "baseName" + archiveAppendixOldC = "appendix" + archiveClassifierOldC = "classifier" + archiveVersionOldC = "version" + archiveExtensionOldC = "extension" + projectPrefixC = "project." + rootProjectPrefixC = "rootProject." + rootProjectNameC = rootProjectPrefixC + "name" + destinationDirectoryC = "destinationDirectory" + destinationDirOldC = "destinationDir" + projectLibsDirNameC = "libsDirName" + buildDirC = "buildDir" + languageVersionC = "languageVersion" + sourceCompatibilityC = "sourceCompatibility" + targetCompatibilityC = "targetCompatibility" + dirFnNameC = "layout.buildDirectory.dir" + projectNameC = "name" + gradleDefaultBuildDirC = "build" // https://docs.gradle.org/current/userguide/writing_build_scripts.html#sec:standard_project_properties + gradleDefaultLibsDirC = "libs" // https://stackoverflow.com/questions/41309257/why-gradle-jars-are-written-in-build-libs + gradleShadowJarPluginC = "com.github.johnrengelman.shadow" + gradleShadowJarPluginBlockC = "shadowJar" + gradleShadowJarPluginDefaultClassifierC = "all" // https://imperceptiblethoughts.com/shadow/configuration/#configuring-output-name + buildStageC = "buildstage" +) + +var ( + // gradleSettingsIncludeRegex is used to match lines containing child module/project paths in settings.gradle. Example: include('web', 'api') + gradleSettingsIncludeRegex = regexp.MustCompile(`^\s*include\(?\s*(?:"[^"]+"|'[^']+')(?:\s*,\s*(?:"[^"]+"|'[^']+'))*\)?$`) + // gradleIndividualProjectRegex is used to extract individual child module/project paths. Example: get 'web' and 'api' from include('web', 'api') + gradleIndividualProjectRegex = regexp.MustCompile(`("[^"]+"|'[^']+')`) +) + +// Init Initializes the transformer +func (t *GradleAnalyser) Init(tc transformertypes.Transformer, env *environment.Environment) (err error) { + t.Config = tc + t.Env = env + t.GradleConfig = &GradleYamlConfig{} + err = common.GetObjFromInterface(t.Config.Spec.Config, t.GradleConfig) + if err != nil { + logrus.Errorf("unable to load config for Transformer %+v into %T : %s", t.Config.Spec.Config, t.GradleConfig, err) + return err + } + if t.GradleConfig.JavaVersion == "" { + t.GradleConfig.JavaVersion = defaultJavaVersion + } + if t.GradleConfig.GradleVersion == "" { + t.GradleConfig.GradleVersion = "7.3" + } + if t.GradleConfig.AppPathInBuildContainer == "" { + t.GradleConfig.AppPathInBuildContainer = defaultAppPathInContainer + } + return nil +} + +// GetConfig returns the transformer config +func (t *GradleAnalyser) GetConfig() (transformertypes.Transformer, *environment.Environment) { + return t.Config, t.Env +} + +// DirectoryDetect runs detect in each sub directory +func (t *GradleAnalyser) DirectoryDetect(dir string) (map[string][]transformertypes.Artifact, error) { + + // look for settings.gradle + + // There will be at most one file path because GetFilesInCurrentDirectory does not check subdirectories. + gradleSettingsFilePaths, err := common.GetFilesInCurrentDirectory(dir, []string{gradleSettingsFileName}, nil) + if err != nil { + return nil, fmt.Errorf("failed to look for %s files in the directory %s . Error: %q", gradleSettingsFileName, dir, err) + } + + // look for a build.gradle as well, in case the root project does not use settings.gradle + + gradleBuildFilePaths, err := common.GetFilesInCurrentDirectory(dir, []string{gradleBuildFileName}, nil) + if err != nil { + return nil, fmt.Errorf("failed to look for %s files in the directory %s . Error: %q", gradleBuildFileName, dir, err) + } + + // if both are missing then skip + + if len(gradleSettingsFilePaths) == 0 && len(gradleBuildFilePaths) == 0 { + return nil, nil + } + + // start filling in the paths for the service artifact + + paths := map[transformertypes.PathType][]string{artifacts.ServiceRootDirPathType: {dir}} + if len(gradleSettingsFilePaths) > 0 { + paths[artifacts.GradleSettingsFilePathType] = gradleSettingsFilePaths + } + if len(gradleBuildFilePaths) > 0 { + paths[artifacts.GradleBuildFilePathType] = gradleBuildFilePaths + } + + // start filling in the gradle config object for the service artifact + + gradleConfig := artifacts.GradleConfig{} + + // check if gradle wrapper script is present + + if gradleWrapperFilePaths, err := common.GetFilesInCurrentDirectory(dir, []string{"gradlew"}, nil); err == nil && len(gradleWrapperFilePaths) != 0 { + gradleConfig.IsGradlewPresent = true + } + + if len(gradleSettingsFilePaths) != 0 { + // found a settings.gradle + + gradleSettingsFilePath := gradleSettingsFilePaths[0] + + // parse the settings.gradle to get the name and child modules of the root project + + gradleSettings, err := gradle.ParseGardleBuildFile(gradleSettingsFilePath) + if err != nil { + return nil, fmt.Errorf("failed to parse the gradle settings script at path %s . Error: %q", gradleSettingsFilePath, err) + } + + // get the name of the root project + + if gradleSettings.Metadata != nil && len(gradleSettings.Metadata[rootProjectNameC]) != 0 { + gradleConfig.RootProjectName = gradleSettings.Metadata[rootProjectNameC][0] + } + + // get the child modules of the root project + + gradleConfig.ChildModules, err = getChildModules(gradleSettingsFilePath) + if err != nil { + return nil, fmt.Errorf("failed to get the child modules from the %s file at path %s . Error: %q", gradleSettingsFileName, gradleSettingsFilePath, err) + } + } + + // if the root project name is not set in settings.gradle then by default the directory name is used + + if gradleConfig.RootProjectName == "" { + gradleConfig.RootProjectName = filepath.Base(dir) + } + + // Get the paths to each of the child module directories (relative to the root project directory). + // These directories will be ignored during the rest of the planning (directory detect) phase. + + if len(gradleConfig.ChildModules) != 0 { + gradleConfig.PackagingType = artifacts.PomPackaging // same packaging as maven multi-module project + for _, childModule := range gradleConfig.ChildModules { + childBuildScriptPath := filepath.Join(dir, childModule.RelBuildScriptPath) + paths[artifacts.ServiceDirPathType] = append(paths[artifacts.ServiceDirPathType], filepath.Dir(childBuildScriptPath)) + } + } else { + + // if there are no child modules then there must be a build.gradle right next to the settings.gradle + + if len(gradleBuildFilePaths) == 0 { + return nil, fmt.Errorf("expected to find a %s file in the directory %s", gradleBuildFileName, dir) + } + paths[artifacts.ServiceDirPathType] = []string{dir} + gradleConfig.ChildModules = []artifacts.GradleChildModule{{Name: gradleConfig.RootProjectName, RelBuildScriptPath: gradleBuildFileName}} + + // we can get the packaging type (jar/war/ear) by parsing this build.gradle file + + gradleBuildFilePath := gradleBuildFilePaths[0] + gradleBuild, err := gradle.ParseGardleBuildFile(gradleBuildFilePath) + if err != nil { + return nil, fmt.Errorf("failed to parse the gradle build script at path %s . Error: %q", gradleBuildFilePath, err) + } + gradleConfig.PackagingType = getPackagingFromGradle(&gradleBuild) + + // if nothing was found in the build.gradle file then assume jar packaging + + if gradleConfig.PackagingType == "" { + gradleConfig.PackagingType = artifacts.JarPackaging + } + } + + serviceArtifact := transformertypes.Artifact{ + Paths: paths, + Configs: map[transformertypes.ConfigType]interface{}{artifacts.GradleConfigType: gradleConfig}, + } + normalizedServiceName := common.MakeStringK8sServiceNameCompliant(gradleConfig.RootProjectName) + services := map[string][]transformertypes.Artifact{normalizedServiceName: {serviceArtifact}} + return services, nil +} + +// Transform transforms the input artifacts, mostly handles artifacts created during the plan phase. +func (t *GradleAnalyser) Transform(newArtifacts []transformertypes.Artifact, alreadySeenArtifacts []transformertypes.Artifact) ([]transformertypes.PathMapping, []transformertypes.Artifact, error) { + pathMappings := []transformertypes.PathMapping{} + createdArtifacts := []transformertypes.Artifact{} + + for _, newArtifact := range newArtifacts { + + // only process service artifacts + + if newArtifact.Type != artifacts.ServiceArtifactType { + continue + } + + serviceConfig := artifacts.ServiceConfig{} + if err := newArtifact.GetConfig(artifacts.ServiceConfigType, &serviceConfig); err != nil { + logrus.Debugf("unable to load config for Transformer into %T : %s", serviceConfig, err) + continue + } + + // only process artifacts that have gradle config + + gradleConfig := artifacts.GradleConfig{} + if err := newArtifact.GetConfig(artifacts.GradleConfigType, &gradleConfig); err != nil { + logrus.Debugf("failed to load the gradle config object from the artifact %+v . Error: %q", newArtifact, err) + continue + } + + // there must be a settings.gradle or a build.gradle in the root project directory + + if len(newArtifact.Paths[artifacts.GradleSettingsFilePathType]) == 0 && len(newArtifact.Paths[artifacts.GradleBuildFilePathType]) == 0 { + logrus.Errorf("the artifact doesn't contain any settings.gradle or build.gradle file paths. Artifact: %+v", newArtifact) + continue + } + + // there must be a root project directory in the list of paths + + if len(newArtifact.Paths[artifacts.ServiceRootDirPathType]) == 0 { + logrus.Errorf("the service root directory is missing for the artifact: %+v", newArtifact) + continue + } + + // transform a single service artifact (probably created during the plan phase by GradleAnalyser.DirectoryDetect) + + currPathMappings, currCreatedArtifacts, err := t.TransformArtifact(newArtifact, alreadySeenArtifacts, serviceConfig, gradleConfig) + if err != nil { + logrus.Errorf("failed to transform the artifact: %+v . Error: %q", newArtifact, err) + continue + } + pathMappings = append(pathMappings, currPathMappings...) + createdArtifacts = append(createdArtifacts, currCreatedArtifacts...) + } + + return pathMappings, createdArtifacts, nil +} + +// TransformArtifact transforms a single artifact. +func (t *GradleAnalyser) TransformArtifact(newArtifact transformertypes.Artifact, alreadySeenArtifacts []transformertypes.Artifact, serviceConfig artifacts.ServiceConfig, gradleConfig artifacts.GradleConfig) ([]transformertypes.PathMapping, []transformertypes.Artifact, error) { + pathMappings := []transformertypes.PathMapping{} + createdArtifacts := []transformertypes.Artifact{} + + selectedBuildOption, err := askUserForDockerfileType(gradleConfig.RootProjectName) + if err != nil { + return pathMappings, createdArtifacts, err + } + logrus.Debugf("user chose to generate Dockefiles that have '%s'", selectedBuildOption) + + // ask the user which child modules should be run in the K8s cluster + + selectedChildModuleNames := []string{} + for _, childModule := range gradleConfig.ChildModules { + selectedChildModuleNames = append(selectedChildModuleNames, childModule.Name) + } + if len(selectedChildModuleNames) > 1 { + //TODO: WASI + //quesKey := fmt.Sprintf(common.ConfigServicesChildModulesNamesKey, `"`+serviceConfig.ServiceName+`"`) + //desc := fmt.Sprintf("For the multi-module Gradle project '%s', please select all the child modules that should be run as services in the cluster:", serviceConfig.ServiceName) + //hints := []string{"deselect child modules that should not be run (like libraries)"} + //selectedChildModuleNames = qaengine.FetchMultiSelectAnswer(quesKey, desc, hints, selectedChildModuleNames, selectedChildModuleNames, nil) + if len(selectedChildModuleNames) == 0 { + return pathMappings, createdArtifacts, fmt.Errorf("user deselected all the child modules of the gradle multi-module project '%s'", serviceConfig.ServiceName) + } + } + + // have jar/war/ear analyzer transformers generate a Dockerfile with only the run stage for each of the child modules + + lowestJavaVersion := "" + imageToCopyFrom := serviceConfig.ServiceName + "-" + buildStageC + serviceRootDir := newArtifact.Paths[artifacts.ServiceRootDirPathType][0] + + for _, childModule := range gradleConfig.ChildModules { + + // only look at the child modules the user selected + + if !common.IsPresent(selectedChildModuleNames, childModule.Name) { + continue + } + + // parse the build.gradle of this child module + + childGradleBuildFilePath := filepath.Join(serviceRootDir, childModule.RelBuildScriptPath) + childGradleBuild, err := gradle.ParseGardleBuildFile(childGradleBuildFilePath) + if err != nil { + logrus.Errorf("failed to parse the gradle build script at path %s . Error: %q", childGradleBuildFilePath, err) + continue + } + + // get some info about the child module to fill the artifact + + childModuleInfo, err := getInfoFromBuildGradle(&childGradleBuild, childGradleBuildFilePath, gradleConfig, childModule) + if err != nil { + logrus.Errorf("failed to get information from the child build.gradle %+v . Error: %q", childGradleBuild, err) + continue + } + + // Find the lowest java version among all of the child modules. + // We will use this java version while doing the build. + + if lowestJavaVersion == "" { + lowestJavaVersion = childModuleInfo.JavaVersion + } + + // have the user select which spring boot profiles to use and find a suitable list of ports + + //desc := fmt.Sprintf("Select the spring boot profiles for the service '%s' :", childModule.Name) + //hints := []string{"select all the profiles that are applicable"} + detectedPorts := []int32{} + envVarsMap := map[string]string{} + if childModuleInfo.SpringBoot != nil { + if childModuleInfo.SpringBoot.SpringBootProfiles != nil && len(*childModuleInfo.SpringBoot.SpringBootProfiles) != 0 { + //TODO: WASI + //quesKey := fmt.Sprintf(common.ConfigServicesChildModulesSpringProfilesKey, `"`+serviceConfig.ServiceName+`"`, `"`+childModule.Name+`"`) + //selectedSpringProfiles := qaengine.FetchMultiSelectAnswer(quesKey, desc, hints, *childModuleInfo.SpringBoot.SpringBootProfiles, *childModuleInfo.SpringBoot.SpringBootProfiles, nil) + selectedSpringProfiles := []string{""} + for _, selectedSpringProfile := range selectedSpringProfiles { + detectedPorts = append(detectedPorts, childModuleInfo.SpringBoot.SpringBootProfilePorts[selectedSpringProfile]...) + } + envVarsMap["SPRING_PROFILES_ACTIVE"] = strings.Join(selectedSpringProfiles, ",") + } else { + detectedPorts = childModuleInfo.SpringBoot.SpringBootProfilePorts[defaultSpringProfile] + } + } + + // have the user select the port to use + + //TODO: WASI + //selectedPort := commonqa.GetPortForService(detectedPorts, common.JoinQASubKeys(`"`+serviceConfig.ServiceName+`"`, "childModules", `"`+childModule.Name+`"`)) + selectedPort := int32(9001) + if childModuleInfo.SpringBoot != nil { + envVarsMap["SERVER_PORT"] = cast.ToString(selectedPort) + } else { + envVarsMap["PORT"] = cast.ToString(selectedPort) + } + + // find the path to the artifact (jar/war/ear) which should get copied into the run stage + + relDeploymentFilePath, err := filepath.Rel(serviceRootDir, childModuleInfo.DeploymentFilePath) + if err != nil { + logrus.Errorf("failed to make the path %s relative to the base directory %s . Error: %q", childModuleInfo.DeploymentFilePath, serviceRootDir, err) + continue + } + insideContainerDepFilePath := filepath.Join(t.GradleConfig.AppPathInBuildContainer, relDeploymentFilePath) + childModuleDir := filepath.Dir(childGradleBuildFilePath) + if selectedBuildOption == NO_BUILD_STAGE { + imageToCopyFrom = "" + relDeploymentFilePath, err = filepath.Rel(childModuleDir, childModuleInfo.DeploymentFilePath) + if err != nil { + logrus.Errorf("failed to make the jar/war/ear archive path %s relative to the child module service directory %s . Error: %q", childModuleInfo.DeploymentFilePath, childModuleDir, err) + continue + } + insideContainerDepFilePath = relDeploymentFilePath + } + + // create an artifact that will get picked up by the jar/war/ear analyzer transformers + + runStageArtifact := transformertypes.Artifact{ + Name: childModule.Name, + Type: childModuleInfo.Type, + Paths: map[transformertypes.PathType][]string{artifacts.ServiceDirPathType: {childModuleDir}}, + Configs: map[transformertypes.ConfigType]interface{}{ + transformertypes.ConfigType(childModuleInfo.Type): artifacts.JarArtifactConfig{ + Port: selectedPort, + JavaVersion: childModuleInfo.JavaVersion, + BuildContainerName: imageToCopyFrom, + DeploymentFilePath: insideContainerDepFilePath, + EnvVariables: envVarsMap, + }, + artifacts.ImageNameConfigType: artifacts.ImageName{ImageName: common.MakeStringContainerImageNameCompliant(childModule.Name)}, + artifacts.ServiceConfigType: artifacts.ServiceConfig{ServiceName: common.MakeStringK8sServiceNameCompliant(childModule.Name)}, + }, + } + createdArtifacts = append(createdArtifacts, runStageArtifact) + } + + if selectedBuildOption == NO_BUILD_STAGE { + return pathMappings, createdArtifacts, nil + } + + // Find the java package corresponding to the java version. + // This will be installed inside the build stage Dockerfile. + + if lowestJavaVersion == "" { + lowestJavaVersion = t.GradleConfig.JavaVersion + } + javaPackageName, err := t.getJavaPackage(lowestJavaVersion) + if err != nil { + return pathMappings, createdArtifacts, fmt.Errorf("failed to get the java package for the java version %s . Error: %q", lowestJavaVersion, err) + } + + // write the build stage Dockerfile template to a temporary file for the pathmapping to pick it up + + dockerfileTemplate, err := t.getDockerfileTemplate() + if err != nil { + return pathMappings, createdArtifacts, fmt.Errorf("failed to get the Dockerfile template. Error: %q", err) + } + tempDir, err := os.MkdirTemp(t.Env.TempPath, "gradle-transformer-build-*") + if err != nil { + return pathMappings, createdArtifacts, fmt.Errorf("failed to create a temporary directory inside the directory %s . Error: %q", t.Env.TempPath, err) + } + dockerfileTemplatePath := filepath.Join(tempDir, common.DefaultDockerfileName+".build.template") + if err := os.WriteFile(dockerfileTemplatePath, []byte(dockerfileTemplate), common.DefaultFilePermission); err != nil { + return pathMappings, createdArtifacts, fmt.Errorf("failed to write the Dockerfile template to a temporary file at path %s . Error: %q", dockerfileTemplatePath, err) + } + + // the build stage Dockefile should be placed in the root project directory + + relServiceRootDir, err := filepath.Rel(t.Env.GetEnvironmentSource(), serviceRootDir) + if err != nil { + return pathMappings, createdArtifacts, fmt.Errorf("failed to make the service root directory %s relative to the source code directory %s . Error: %q", serviceRootDir, t.Env.GetEnvironmentSource(), err) + } + dockerfilePath := filepath.Join(common.DefaultSourceDir, relServiceRootDir, common.DefaultDockerfileName+"."+buildStageC) + + // fill in the Dockerfile template for the build stage and write it out using a pathmapping + + buildStageDockerfilePathMapping := transformertypes.PathMapping{ + Type: transformertypes.TemplatePathMappingType, + SrcPath: dockerfileTemplatePath, + DestPath: dockerfilePath, + TemplateConfig: GradleBuildDockerfileTemplate{ + GradlewPresent: gradleConfig.IsGradlewPresent, + JavaPackageName: javaPackageName, + GradleVersion: t.GradleConfig.GradleVersion, + BuildContainerName: imageToCopyFrom, + GradleProperties: map[string]string{}, // TODO: gather gradle properties maybe? analog for maven is info.MavenProfiles. https://www.credera.com/insights/gradle-profiles-for-multi-project-spring-boot-applications + EnvVariables: map[string]string{}, // TODO: Something about getting env vars from the IR config inside the artifact coming from the cloud foundry transformer? + }, + } + + if selectedBuildOption == BUILD_IN_EVERY_IMAGE { + dockerfilePath = filepath.Join(tempDir, common.DefaultDockerfileName+"."+buildStageC) + buildStageDockerfilePathMapping.DestPath = dockerfilePath + for _, createdArtifact := range createdArtifacts { + createdArtifact.Paths[artifacts.BuildContainerFileType] = []string{dockerfilePath} + } + } else { + + // make sure the source code directory has been copied over first + + copySourceDirPathMapping := transformertypes.PathMapping{ + Type: transformertypes.SourcePathMappingType, + DestPath: common.DefaultSourceDir, + } + pathMappings = append(pathMappings, copySourceDirPathMapping) + + // Tell the other transformers about the build stage Dockerfile we created. + // That way, the image will get built by the builddockerimages.sh script. + + baseImageDockerfileArtifact := transformertypes.Artifact{ + Name: imageToCopyFrom, + Type: artifacts.DockerfileArtifactType, + Paths: map[transformertypes.PathType][]string{artifacts.DockerfilePathType: {dockerfilePath}}, // TODO: should we add the context path as well? + Configs: map[transformertypes.ConfigType]interface{}{artifacts.ImageNameConfigType: artifacts.ImageName{ImageName: imageToCopyFrom}}, + } + createdArtifacts = append(createdArtifacts, baseImageDockerfileArtifact) + } + pathMappings = append(pathMappings, buildStageDockerfilePathMapping) + + return pathMappings, createdArtifacts, nil +} + +// func getInfoFromSettingsGradle(gradleSettingsPtr *gradle.Gradle, gradleSettingsFilePath string, gradleConfig artifacts.GradleConfig) (gradleInfoT, error) { +// info := gradleInfoT{ +// Name: gradleConfig.RootProjectName, +// IsParentBuild: len(gradleConfig.ChildModules) > 0, +// IsGradlewPresent: gradleConfig.IsGradlewPresent, +// ChildModules: gradleConfig.ChildModules, +// } +// return info, nil +// } + +func getInfoFromBuildGradle(gradleBuildPtr *gradle.Gradle, gradleBuildFilePath string, gradleConfig artifacts.GradleConfig, childModule artifacts.GradleChildModule) (gradleInfoT, error) { + info := gradleInfoT{ + Name: childModule.Name, + } // TODO: multiple levels of sub-projects + gradleBuildFileDir := filepath.Dir(gradleBuildFilePath) + if ps, err := common.GetFilesInCurrentDirectory(gradleBuildFileDir, []string{"gradlew"}, nil); err != nil { + return info, fmt.Errorf("failed to look for gradle wrapper files in the child module service directory %s . Error: %q", gradleBuildFileDir, err) + } else if len(ps) > 0 { + info.IsGradlewPresent = true + } + if gradleBuildPtr != nil { + gradleBuild := *gradleBuildPtr + info.SpringBoot = getSpringBootConfigFromGradle(gradleBuildFilePath, gradleBuildPtr, nil) + packType := getPackagingFromGradle(gradleBuildPtr) + if packType == "" { + packType = artifacts.JarPackaging + } + artType, err := packagingToArtifactType(packType) + if err != nil { + return info, fmt.Errorf("failed to convert the packaging type %s to a valid artifact type. Error: %q", gradleConfig.PackagingType, err) + } + info.Type = artType + info.JavaVersion = getJavaVersionFromGradle(gradleBuildPtr) + + deploymentFilePath, err := getDeploymentFilePathFromGradle(gradleBuildPtr, gradleBuildFilePath, filepath.Dir(gradleBuildFilePath), gradleConfig, packType) + if err != nil { + return info, fmt.Errorf("failed to get the output path for the gradle build script %+v . Error: %q", gradleBuild, err) + } + info.DeploymentFilePath = deploymentFilePath + } + return info, nil +} + +func getPackagingFromGradle(gradleBuild *gradle.Gradle) artifacts.JavaPackaging { + if gradleBuild == nil { + return "" + } + pluginIds := gradleBuild.GetPluginIDs() + if common.IsPresent(pluginIds, string(artifacts.JarPackaging)) { + return artifacts.JarPackaging + } else if common.IsPresent(pluginIds, string(artifacts.EarPackaging)) { + return artifacts.EarPackaging + } else if common.IsPresent(pluginIds, string(artifacts.WarPackaging)) { + return artifacts.WarPackaging + } + return "" +} + +func getSpringBootConfigFromGradle(buildFilePath string, gradleBuild, parentGradleBuild *gradle.Gradle) *artifacts.SpringBootConfig { + if gradleBuild == nil { + logrus.Errorf("got a nil gradle build script") + return nil + } + buildFileDir := filepath.Dir(buildFilePath) + getSpringBootConfig := func(dependency gradle.GradleDependency) *artifacts.SpringBootConfig { + if dependency.Group != springBootGroup { + return nil + } + springAppName, springProfiles, profilePorts := getSpringBootAppNameProfilesAndPortsFromDir(buildFileDir) + springConfig := &artifacts.SpringBootConfig{ + SpringBootVersion: dependency.Version, + SpringBootAppName: springAppName, + SpringBootProfilePorts: profilePorts, + } + if len(springProfiles) != 0 { + springConfig.SpringBootProfiles = &springProfiles + } + return springConfig + } + // look for spring boot + for _, dependency := range gradleBuild.Dependencies { + if springConfig := getSpringBootConfig(dependency); springConfig != nil { + return springConfig + } + } + return nil +} + +func (t *GradleAnalyser) getDockerfileTemplate() (string, error) { + // TODO: see if we can cache gradle dependencies similar to https://stackoverflow.com/a/37442191 + licenseFilePath := filepath.Join(t.Env.GetEnvironmentContext(), t.Env.RelTemplatesDir, "Dockerfile.license") + license, err := os.ReadFile(licenseFilePath) + if err != nil { + return "", fmt.Errorf("failed to read the Dockerfile license file at path %s . Error: %q", licenseFilePath, err) + } + gradleBuildTemplatePath := filepath.Join(t.Env.GetEnvironmentContext(), t.Env.RelTemplatesDir, "Dockerfile.gradle-build") + gradleBuildTemplate, err := os.ReadFile(gradleBuildTemplatePath) + if err != nil { + return string(license), fmt.Errorf("failed to read the Dockerfile Gradle build template file at path %s . Error: %q", gradleBuildTemplatePath, err) + } + return string(license) + "\n" + string(gradleBuildTemplate), nil +} + +func (t *GradleAnalyser) getJavaPackage(javaVersion string) (string, error) { + javaVersionToPackageMappingFilePath := filepath.Join(t.Env.GetEnvironmentContext(), versionMappingFilePath) + return getJavaPackage(javaVersionToPackageMappingFilePath, javaVersion) +} + +// getJavaVersionFromGradle finds the java version from a gradle build script (build.gradle). +func getJavaVersionFromGradle(buildGradleFile *gradle.Gradle) string { + if buildGradleFile == nil || buildGradleFile.Blocks == nil { + return "" + } + // https://docs.gradle.org/current/userguide/java_plugin.html#sec:java-extension + if gb, ok := buildGradleFile.Blocks["java"]; ok { + if gbb, ok := gb.Blocks["toolchain"]; ok { + if gbb.Metadata != nil && len(gbb.Metadata[languageVersionC]) > 0 { + ss := gradle.GetSingleArgumentFromFuntionCall(gbb.Metadata[languageVersionC][0], "JavaLanguageVersion.of") + gradleJavaVersion, err := cast.ToIntE(ss) + if err != nil { + logrus.Errorf("failed to parse the string '%s' as an integer. Error: %q", ss, err) + return "" + } + if gradleJavaVersion < 10 { + return "1." + cast.ToString(gradleJavaVersion) + } + return cast.ToString(gradleJavaVersion) + } + } else if gb.Metadata != nil { + sourceOrTargetVersions := gb.Metadata[targetCompatibilityC] + if len(sourceOrTargetVersions) == 0 { + sourceOrTargetVersions = gb.Metadata[sourceCompatibilityC] + } + if len(sourceOrTargetVersions) > 0 { + sourceOrTargetVersion := sourceOrTargetVersions[0] + return sourceOrTargetVersion + } + } + } + return "" +} + +func getDeploymentFilePathFromGradle(gradleBuild *gradle.Gradle, buildScriptPath, serviceDir string, gradleConfig artifacts.GradleConfig, packagingType artifacts.JavaPackaging) (string, error) { + if gradleBuild == nil { + return "", fmt.Errorf("the given gradle build script is nil") + } + archivePath := "" + archiveName := "" + destinationPath := "" + archiveBaseName := "" + archiveAppendix := "" + archiveVersion := "" + archiveClassifier := "" + archiveExtension := "" + joinIntoName := func() string { + ans := archiveBaseName + if archiveAppendix != "" { + ans += "-" + archiveAppendix + } + if archiveVersion != "" { + ans += "-" + archiveVersion + } + if archiveClassifier != "" { + ans += "-" + archiveClassifier + } + if archiveExtension != "" { + ans += "." + archiveExtension + } + return ans + } + + updateArchiveNameFromJarBlock := func(gb gradle.Gradle) { + if gb.Metadata == nil { + return + } + // https://docs.gradle.org/current/dsl/org.gradle.api.tasks.bundling.Jar.html#org.gradle.api.tasks.bundling.Jar:archiveFile + if len(gb.Metadata[archiveFileC]) > 0 { + archivePath = gb.Metadata[archiveFileC][0] + } + // https://docs.gradle.org/current/dsl/org.gradle.api.tasks.bundling.Jar.html#org.gradle.api.tasks.bundling.Jar:destinationDirectory + if len(gb.Metadata[destinationDirectoryC]) > 0 { + destinationPath = gradle.GetSingleArgumentFromFuntionCall(gb.Metadata[destinationDirectoryC][0], dirFnNameC) + } else if len(gb.Metadata[destinationDirOldC]) > 0 { + destinationPath = gradle.GetSingleArgumentFromFuntionCall(gb.Metadata[destinationDirOldC][0], dirFnNameC) + } + // https://docs.gradle.org/current/dsl/org.gradle.api.tasks.bundling.Jar.html#org.gradle.api.tasks.bundling.Jar:archiveFileName + if len(gb.Metadata[archiveFileNameC]) > 0 { + archiveName = gb.Metadata[archiveFileNameC][0] + } else if len(gb.Metadata[archiveNameC]) > 0 { + archiveName = gb.Metadata[archiveNameC][0] + } + if archiveName == "" { + // look for ${archiveBaseName}-${archiveAppendix}-${archiveVersion}-${archiveClassifier}.${archiveExtension} + // archiveBaseName + if len(gb.Metadata[archiveBaseNameC]) > 0 { + archiveBaseName = gb.Metadata[archiveBaseNameC][0] + } else if len(gb.Metadata[archiveBaseNameOldC]) > 0 { + archiveBaseName = gb.Metadata[archiveBaseNameOldC][0] + } else if len(gradleBuild.Metadata[projectNameC]) > 0 { + // TODO: project.name is a read-only property + // https://docs.gradle.org/current/dsl/org.gradle.api.Project.html#org.gradle.api.Project:name + // https://stackoverflow.com/a/55690608 + archiveBaseName = gradleBuild.Metadata[projectNameC][0] + } else { + archiveBaseName = filepath.Base(filepath.Dir(buildScriptPath)) + } + // archiveAppendix + if len(gb.Metadata[archiveAppendixC]) > 0 { + archiveAppendix = gb.Metadata[archiveAppendixC][0] + } else if len(gb.Metadata[archiveAppendixOldC]) > 0 { + archiveAppendix = gb.Metadata[archiveAppendixOldC][0] + } else if len(gradleBuild.Metadata[projectPrefixC+archiveAppendixOldC]) > 0 { + archiveAppendix = gradleBuild.Metadata[projectPrefixC+archiveAppendixOldC][0] + } else if len(gradleBuild.Metadata[archiveAppendixOldC]) > 0 { + archiveAppendix = gradleBuild.Metadata[archiveAppendixOldC][0] + } + // archiveVersion + if len(gb.Metadata[archiveVersionC]) > 0 { + archiveVersion = gb.Metadata[archiveVersionC][0] + } else if len(gb.Metadata[archiveVersionOldC]) > 0 { + archiveVersion = gb.Metadata[archiveVersionOldC][0] + } else if len(gradleBuild.Metadata[projectPrefixC+archiveVersionOldC]) > 0 { + archiveVersion = gradleBuild.Metadata[projectPrefixC+archiveVersionOldC][0] + } else if len(gradleBuild.Metadata[archiveVersionOldC]) > 0 { + archiveVersion = gradleBuild.Metadata[archiveVersionOldC][0] + } + // archiveClassifier + if len(gb.Metadata[archiveClassifierC]) > 0 { + archiveClassifier = gb.Metadata[archiveClassifierC][0] + } else if len(gb.Metadata[archiveClassifierOldC]) > 0 { + archiveClassifier = gb.Metadata[archiveClassifierOldC][0] + } else if len(gradleBuild.Metadata[projectPrefixC+archiveClassifierOldC]) > 0 { + archiveClassifier = gradleBuild.Metadata[projectPrefixC+archiveClassifierOldC][0] + } else if len(gradleBuild.Metadata[archiveClassifierOldC]) > 0 { + archiveClassifier = gradleBuild.Metadata[archiveClassifierOldC][0] + } + // archiveExtension + if len(gb.Metadata[archiveExtensionC]) > 0 { + archiveExtension = gb.Metadata[archiveExtensionC][0] + } else if len(gb.Metadata[archiveExtensionOldC]) > 0 { + archiveExtension = gb.Metadata[archiveExtensionOldC][0] + } else { + archiveExtension = string(packagingType) + } + } + } + + // first we look in the top level for the version + + // archiveBaseName + // project.name is a read-only property + // https://docs.gradle.org/current/dsl/org.gradle.api.Project.html#org.gradle.api.Project:name + // https://stackoverflow.com/a/55690608 + archiveBaseName = filepath.Base(filepath.Dir(buildScriptPath)) + // archiveVersion + if len(gradleBuild.Metadata[projectPrefixC+archiveVersionOldC]) > 0 { + archiveVersion = gradleBuild.Metadata[projectPrefixC+archiveVersionOldC][0] + } else if len(gradleBuild.Metadata[archiveVersionOldC]) > 0 { + archiveVersion = gradleBuild.Metadata[archiveVersionOldC][0] + } + // archiveExtension + archiveExtension = string(packagingType) + + // second we look in the shadowJar block to override the archive name + + if common.IsPresent(gradleBuild.GetPluginIDs(), gradleShadowJarPluginC) { + archiveClassifier = gradleShadowJarPluginDefaultClassifierC + if gradleBuild.Blocks != nil { + if gb2, ok := gradleBuild.Blocks[gradleShadowJarPluginBlockC]; ok { + updateArchiveNameFromJarBlock(gb2) + } + } + } + + rootGradlePropertiesPaths, err := common.GetFilesInCurrentDirectory(serviceDir, []string{"gradle.properties"}, nil) + if err != nil { + logrus.Errorf("failed to look for a gradle.properties file in the directory %s . Error: %q", serviceDir, err) + } else if len(rootGradlePropertiesPaths) > 0 { + if len(rootGradlePropertiesPaths) > 1 { + logrus.Errorf("expected there to be at most one gradle.properties file. actual: %+v", rootGradlePropertiesPaths) + } + rootGradlePropertiesPath := rootGradlePropertiesPaths[0] + rootGradleProperties, err := properties.LoadFile(rootGradlePropertiesPath, properties.UTF8) + if err != nil { + logrus.Errorf("failed to parse the gradle.properties at path %s . Error: %q", rootGradlePropertiesPath, err) + } + if v, ok := rootGradleProperties.Get("version"); ok { + archiveVersion = v + } + } + + if archivePath != "" { + return filepath.Join(serviceDir, archivePath), nil + } + + // third we look in the jar/war/ear block to override the archive name + if gradleBuild.Blocks != nil { + if gb1, ok := gradleBuild.Blocks[string(packagingType)]; ok { + updateArchiveNameFromJarBlock(gb1) + } + } + if archivePath != "" { + return filepath.Join(serviceDir, archivePath), nil + } + + // get the archiveName by combining the different parts + archiveName = joinIntoName() + + if destinationPath != "" { + return filepath.Join(serviceDir, destinationPath, archiveName), nil + } + // libs directory where the archives are genearted by the jar/war/ear plugin + if len(gradleBuild.Metadata[projectPrefixC+projectLibsDirNameC]) > 0 { + destinationPath = gradleBuild.Metadata[projectPrefixC+projectLibsDirNameC][0] + } else if len(gradleBuild.Metadata[projectLibsDirNameC]) > 0 { + destinationPath = gradleBuild.Metadata[projectLibsDirNameC][0] + } else { + destinationPath = gradleDefaultLibsDirC + } + // find the build output directory + // https://docs.gradle.org/current/dsl/org.gradle.api.Project.html#org.gradle.api.Project:buildDir + buildDir := "" + if len(gradleBuild.Metadata[projectPrefixC+buildDirC]) > 0 { + buildDir = gradleBuild.Metadata[projectPrefixC+buildDirC][0] + } else if len(gradleBuild.Metadata[buildDirC]) > 0 { + buildDir = gradleBuild.Metadata[buildDirC][0] + } else { + buildDir = gradleDefaultBuildDirC + } + return filepath.Join(serviceDir, buildDir, destinationPath, archiveName), nil +} + +func getChildModules(gradleSettingsFilePath string) ([]artifacts.GradleChildModule, error) { + gradleSettingsFile, err := os.Open(gradleSettingsFilePath) + if err != nil { + return nil, fmt.Errorf("failed to open the settings.gradle file at path %s . Error: %q", gradleSettingsFilePath, err) + } + scanner := bufio.NewScanner(gradleSettingsFile) + scanner.Split(bufio.ScanLines) + childModuleRelPaths := []string{} + for scanner.Scan() { + line := scanner.Text() + if gradleSettingsIncludeRegex.MatchString(line) { + resultss := gradleIndividualProjectRegex.FindAllStringSubmatch(line, -1) + for _, results := range resultss { + if len(results) == 0 { + continue + } + quotedChildModuleRelPath := results[0] + if len(quotedChildModuleRelPath) < 3 { + logrus.Debugf("invalid or empty child module path. Actual: %s\n", quotedChildModuleRelPath) + continue + } + childModuleRelPath := quotedChildModuleRelPath[1 : len(quotedChildModuleRelPath)-1] + childModuleRelPaths = append(childModuleRelPaths, childModuleRelPath) + } + } + } + childModules := []artifacts.GradleChildModule{} + for _, childModuleRelPath := range childModuleRelPaths { + childModuleRelPath = strings.Replace(childModuleRelPath, ":", string(os.PathSeparator), -1) + if filepath.IsAbs(childModuleRelPath) { + childModuleRelPath = childModuleRelPath[1:] + } + name := filepath.Base(childModuleRelPath) + relPath := filepath.Join(childModuleRelPath, gradleBuildFileName) + childModules = append(childModules, artifacts.GradleChildModule{Name: name, RelBuildScriptPath: relPath}) + } + if err := scanner.Err(); err != nil { + return childModules, fmt.Errorf("failed to read the gradle settings script at path %s line by line. Error: %q", gradleSettingsFilePath, err) + } + return childModules, nil +} diff --git a/transformer/dockerfilegenerator/java/jaranalyser.go b/transformer/dockerfilegenerator/java/jaranalyser.go new file mode 100644 index 0000000..cbc5fc6 --- /dev/null +++ b/transformer/dockerfilegenerator/java/jaranalyser.go @@ -0,0 +1,261 @@ +/* + * Copyright IBM Corporation 2021 + * + * 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 java + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/konveyor/move2kube-wasm/common" + "github.com/konveyor/move2kube-wasm/environment" + //irtypes "github.com/konveyor/move2kube-wasm/types/ir" + transformertypes "github.com/konveyor/move2kube-wasm/types/transformer" + "github.com/konveyor/move2kube-wasm/types/transformer/artifacts" + "github.com/sirupsen/logrus" + "github.com/spf13/cast" +) + +// JarAnalyser implements Transformer interface +type JarAnalyser struct { + Config transformertypes.Transformer + Env *environment.Environment + JarConfig *JarYamlConfig +} + +// JarYamlConfig stores jar related configuration information +type JarYamlConfig struct { + JavaVersion string `yaml:"defaultJavaVersion"` + DefaultPort int32 `yaml:"defaultPort"` +} + +// JarDockerfileTemplate stores parameters for the dockerfile template +type JarDockerfileTemplate struct { + Port int32 + JavaPackageName string + BuildContainerName string + DeploymentFilePath string + DeploymentFilename string + EnvVariables map[string]string +} + +// Init Initializes the transformer +func (t *JarAnalyser) Init(tc transformertypes.Transformer, env *environment.Environment) (err error) { + t.Config = tc + t.Env = env + t.JarConfig = &JarYamlConfig{} + err = common.GetObjFromInterface(t.Config.Spec.Config, t.JarConfig) + if err != nil { + logrus.Errorf("unable to load config for Transformer %+v into %T : %s", t.Config.Spec.Config, t.JarConfig, err) + return err + } + if t.JarConfig.JavaVersion == "" { + t.JarConfig.JavaVersion = defaultJavaVersion + } + if t.JarConfig.DefaultPort == 0 { + t.JarConfig.DefaultPort = common.DefaultServicePort + } + return nil +} + +// GetConfig returns the transformer config +func (t *JarAnalyser) GetConfig() (transformertypes.Transformer, *environment.Environment) { + return t.Config, t.Env +} + +// DirectoryDetect runs detect in each sub directory +func (t *JarAnalyser) DirectoryDetect(dir string) (map[string][]transformertypes.Artifact, error) { + services := map[string][]transformertypes.Artifact{} + paths, err := common.GetFilesInCurrentDirectory(dir, nil, []string{`\.jar$`}) + if err != nil { + return nil, fmt.Errorf("failed to get the jar files in the directory %s . Error: %q", dir, err) + } + if len(paths) == 0 { + return nil, nil + } + for _, path := range paths { + relPath, err := filepath.Rel(t.Env.GetEnvironmentSource(), path) + if err != nil { + logrus.Errorf("failed to make the path %s relative to the sourc code directory %s . Error: %q", path, t.Env.GetEnvironmentSource(), err) + continue + } + serviceName := strings.TrimSuffix(filepath.Base(path), filepath.Ext(path)) + normalizedServiceName := common.MakeStringK8sServiceNameCompliant(serviceName) + newArtifact := transformertypes.Artifact{ + Paths: map[transformertypes.PathType][]string{ + artifacts.JarPathType: {path}, + artifacts.ServiceDirPathType: {dir}, + }, + Configs: map[transformertypes.ConfigType]interface{}{ + artifacts.JarConfigType: artifacts.JarArtifactConfig{ + DeploymentFilePath: relPath, + EnvVariables: map[string]string{"PORT": cast.ToString(t.JarConfig.DefaultPort)}, + Port: t.JarConfig.DefaultPort, + JavaVersion: t.JarConfig.JavaVersion, + }, + }, + } + services[normalizedServiceName] = append(services[normalizedServiceName], newArtifact) + } + return services, nil +} + +// Transform transforms the artifacts +func (t *JarAnalyser) Transform(newArtifacts []transformertypes.Artifact, alreadySeenArtifacts []transformertypes.Artifact) ([]transformertypes.PathMapping, []transformertypes.Artifact, error) { + pathMappings := []transformertypes.PathMapping{} + createdArtifacts := []transformertypes.Artifact{} + for _, newArtifact := range newArtifacts { + if newArtifact.Type != artifacts.ServiceArtifactType && newArtifact.Type != artifacts.JarArtifactType { + continue + } + jarArtifactConfig := artifacts.JarArtifactConfig{} + if err := newArtifact.GetConfig(artifacts.JarConfigType, &jarArtifactConfig); err != nil { + logrus.Debugf("failed to load the JAR config from the artifact %+v . Error: %q", jarArtifactConfig, err) + continue + } + serviceConfig := artifacts.ServiceConfig{} + if err := newArtifact.GetConfig(artifacts.ServiceConfigType, &serviceConfig); err != nil { + logrus.Debugf("failed to load the service config from the artifact %+v . Error: %q", newArtifact, err) + continue + } + imageName := artifacts.ImageName{} + if err := newArtifact.GetConfig(artifacts.ImageNameConfigType, &imageName); err != nil { + logrus.Debugf("failed to load the image name config from the artifact %+v . Error: %q", newArtifact, err) + } + if imageName.ImageName == "" { + imageName.ImageName = common.MakeStringContainerImageNameCompliant(serviceConfig.ServiceName) + } + // get the Dockerfile template + dockerfileTemplate, _, err := t.getDockerfileTemplate(newArtifact) + if err != nil { + logrus.Errorf("failed to get the Dockerfile template for the jar artifact %+v . Error: %q", newArtifact, err) + continue + } + // write the Dockerfile template to a temporary file for a pathmapping to pick it up + tempDir := filepath.Join(t.Env.TempPath, newArtifact.Name) + if err := os.MkdirAll(tempDir, common.DefaultDirectoryPermission); err != nil { + logrus.Errorf("failed to create the temporary directory %s . Error: %q", tempDir, err) + continue + } + dockerfileTemplatePath := filepath.Join(tempDir, common.DefaultDockerfileName) + if err := os.WriteFile(dockerfileTemplatePath, []byte(dockerfileTemplate), common.DefaultFilePermission); err != nil { + logrus.Errorf("failed to write the Dockerfile template at path %s . Error: %q", dockerfileTemplatePath, err) + continue + } + // get the data to fill the Dockerfile template + if len(newArtifact.Paths[artifacts.ServiceDirPathType]) == 0 { + logrus.Errorf("the service directory path is missing for the artifact: %+v", newArtifact) + continue + } + serviceDir := newArtifact.Paths[artifacts.ServiceDirPathType][0] + relSrcPath, err := filepath.Rel(t.Env.GetEnvironmentSource(), serviceDir) + if err != nil { + logrus.Errorf("failed to make the service directory %s relative to the source code directory %s . Error: %q", serviceDir, t.Env.GetEnvironmentSource(), err) + continue + } + dockerfilePath := filepath.Join(common.DefaultSourceDir, relSrcPath, common.DefaultDockerfileName) + if jarArtifactConfig.JavaVersion == "" { + jarArtifactConfig.JavaVersion = t.JarConfig.JavaVersion + } + javaPackage, err := getJavaPackage(filepath.Join(t.Env.GetEnvironmentContext(), versionMappingFilePath), jarArtifactConfig.JavaVersion) + if err != nil { + logrus.Errorf("failed to find the java package for the java version %s . Going with java package %s instead. Error: %q", jarArtifactConfig.JavaVersion, defaultJavaPackage, err) + javaPackage = defaultJavaPackage + } + buildContainerName := jarArtifactConfig.BuildContainerName + pathMappingTemplateConfig := JarDockerfileTemplate{ + Port: jarArtifactConfig.Port, + JavaPackageName: javaPackage, + BuildContainerName: buildContainerName, + DeploymentFilePath: jarArtifactConfig.DeploymentFilePath, + DeploymentFilename: filepath.Base(jarArtifactConfig.DeploymentFilePath), + EnvVariables: jarArtifactConfig.EnvVariables, + } + // Fill the Dockerfile template using a pathmapping. + writeDockerfilePathMapping := transformertypes.PathMapping{ + Type: transformertypes.TemplatePathMappingType, + SrcPath: dockerfileTemplatePath, + DestPath: dockerfilePath, + TemplateConfig: pathMappingTemplateConfig, + } + // Make sure the source code directory has been copied over first. + copySourceDirPathMapping := transformertypes.PathMapping{ + Type: transformertypes.SourcePathMappingType, + DestPath: common.DefaultSourceDir, + } + pathMappings = append(pathMappings, copySourceDirPathMapping, writeDockerfilePathMapping) + // Reference the Dockerfile we created in an artifact for other transformers that consume Dockerfiles. + paths := newArtifact.Paths + paths[artifacts.DockerfilePathType] = []string{dockerfilePath} + dockerfileArtifact := transformertypes.Artifact{ + Name: imageName.ImageName, + Type: artifacts.DockerfileArtifactType, + Paths: paths, + Configs: map[transformertypes.ConfigType]interface{}{ + artifacts.ImageNameConfigType: imageName, + }, + } + dockerfileForServiceArtifact := transformertypes.Artifact{ + Name: serviceConfig.ServiceName, + Type: artifacts.DockerfileForServiceArtifactType, + Paths: paths, + Configs: map[transformertypes.ConfigType]interface{}{ + artifacts.ImageNameConfigType: imageName, + artifacts.ServiceConfigType: serviceConfig, + }, + } + + // preserve the ir config and inject cloud foundry vcap properties if it is present + + //TODO: WASI + //ir := irtypes.IR{} + //if err := newArtifact.GetConfig(irtypes.IRConfigType, &ir); err == nil { + // dockerfileForServiceArtifact.Configs[irtypes.IRConfigType] = ir + //} + + createdArtifacts = append(createdArtifacts, dockerfileArtifact, dockerfileForServiceArtifact) + } + return pathMappings, createdArtifacts, nil +} + +func (t *JarAnalyser) getDockerfileTemplate(newArtifact transformertypes.Artifact) (string, bool, error) { + jarRunDockerfileTemplatePath := filepath.Join(t.Env.GetEnvironmentContext(), t.Env.RelTemplatesDir, "Dockerfile.jar-run") + jarRunDockerfileTemplate, err := os.ReadFile(jarRunDockerfileTemplatePath) + if err != nil { + return "", false, fmt.Errorf("failed to read the JAR run Dockerfile template at path %s . Error: %q", jarRunDockerfileTemplatePath, err) + } + dockerFileHead := "" + isBuildContainerPresent := false + if buildContainerPaths, ok := newArtifact.Paths[artifacts.BuildContainerFileType]; ok && len(buildContainerPaths) > 0 { + isBuildContainerPresent = true + buildStageDockerfilePath := buildContainerPaths[0] + dockerFileHeadBytes, err := os.ReadFile(buildStageDockerfilePath) + if err != nil { + return "", isBuildContainerPresent, fmt.Errorf("failed to read the build stage Dockerfile at path %s . Error: %q", buildStageDockerfilePath, err) + } + dockerFileHead = string(dockerFileHeadBytes) + } else { + licensePath := filepath.Join(t.Env.GetEnvironmentContext(), t.Env.RelTemplatesDir, "Dockerfile.license") + dockerFileHeadBytes, err := os.ReadFile(licensePath) + if err != nil { + return "", isBuildContainerPresent, fmt.Errorf("failed to read the Dockerfile license at path %s . Error: %q", licensePath, err) + } + dockerFileHead = string(dockerFileHeadBytes) + } + return string(dockerFileHead) + "\n" + string(jarRunDockerfileTemplate), isBuildContainerPresent, nil +} diff --git a/transformer/dockerfilegenerator/java/jboss.go b/transformer/dockerfilegenerator/java/jboss.go new file mode 100644 index 0000000..427e83f --- /dev/null +++ b/transformer/dockerfilegenerator/java/jboss.go @@ -0,0 +1,221 @@ +/* + * Copyright IBM Corporation 2021 + * + * 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 java + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/konveyor/move2kube-wasm/common" + "github.com/konveyor/move2kube-wasm/environment" + //irtypes "github.com/konveyor/move2kube-wasm/types/ir" + transformertypes "github.com/konveyor/move2kube-wasm/types/transformer" + "github.com/konveyor/move2kube-wasm/types/transformer/artifacts" + "github.com/sirupsen/logrus" +) + +const ( + defaultJbossPort int32 = 8080 +) + +// Jboss implements Transformer interface +type Jboss struct { + Config transformertypes.Transformer + Env *environment.Environment + JbossConfig *JbossYamlConfig +} + +// JbossYamlConfig stores jar related configuration information +type JbossYamlConfig struct { + JavaVersion string `yaml:"defaultJavaVersion"` +} + +// JbossDockerfileTemplate stores parameters for the dockerfile template +type JbossDockerfileTemplate struct { + JavaPackageName string + DeploymentFilePath string + BuildContainerName string + Port int32 + EnvVariables map[string]string +} + +// Init Initializes the transformer +func (t *Jboss) Init(tc transformertypes.Transformer, env *environment.Environment) (err error) { + t.Config = tc + t.Env = env + t.JbossConfig = &JbossYamlConfig{} + err = common.GetObjFromInterface(t.Config.Spec.Config, t.JbossConfig) + if err != nil { + logrus.Errorf("unable to load config for Transformer %+v into %T : %s", t.Config.Spec.Config, t.JbossConfig, err) + return err + } + if t.JbossConfig.JavaVersion == "" { + t.JbossConfig.JavaVersion = defaultJavaVersion + } + return nil +} + +// GetConfig returns the transformer config +func (t *Jboss) GetConfig() (transformertypes.Transformer, *environment.Environment) { + return t.Config, t.Env +} + +// DirectoryDetect runs detect in each sub directory +func (t *Jboss) DirectoryDetect(dir string) (services map[string][]transformertypes.Artifact, err error) { + return +} + +// Transform transforms the artifacts +func (t *Jboss) Transform(newArtifacts []transformertypes.Artifact, alreadySeenArtifacts []transformertypes.Artifact) ([]transformertypes.PathMapping, []transformertypes.Artifact, error) { + pathMappings := []transformertypes.PathMapping{} + createdArtifacts := []transformertypes.Artifact{} + for _, newArtifact := range newArtifacts { + serviceConfig := artifacts.ServiceConfig{} + if err := newArtifact.GetConfig(artifacts.ServiceConfigType, &serviceConfig); err != nil { + logrus.Errorf("unable to load config for Transformer into %T : %s", serviceConfig, err) + continue + } + if serviceConfig.ServiceName == "" { + serviceConfig.ServiceName = common.MakeStringK8sServiceNameCompliant(newArtifact.Name) + } + imageName := artifacts.ImageName{} + if err := newArtifact.GetConfig(artifacts.ImageNameConfigType, &imageName); err != nil { + logrus.Debugf("unable to load config for Transformer into %T : %s", imageName, err) + } + if imageName.ImageName == "" { + imageName.ImageName = common.MakeStringContainerImageNameCompliant(newArtifact.Name) + } + if len(newArtifact.Paths[artifacts.ServiceDirPathType]) == 0 { + logrus.Errorf("service directory missing from artifact: %+v", newArtifact) + continue + } + serviceDir := newArtifact.Paths[artifacts.ServiceDirPathType][0] + relServiceDir, err := filepath.Rel(t.Env.GetEnvironmentSource(), serviceDir) + if err != nil { + logrus.Errorf("failed to make the service directory %s relative to the source code directory %s . Error: %q", serviceDir, t.Env.GetEnvironmentSource(), err) + continue + } + template, err := t.getDockerfileTemplate(newArtifact) + if err != nil { + logrus.Errorf("failed to get the jboss run stage Dockerfile template. Error: %q", err) + continue + } + tempDir := filepath.Join(t.Env.TempPath, newArtifact.Name) + if err := os.MkdirAll(tempDir, common.DefaultDirectoryPermission); err != nil { + logrus.Errorf("failed to create the temporary directory %s . Error: %q", tempDir, err) + continue + } + dockerfileTemplatePath := filepath.Join(tempDir, common.DefaultDockerfileName) + if err := os.WriteFile(dockerfileTemplatePath, []byte(template), common.DefaultFilePermission); err != nil { + logrus.Errorf("Could not write the generated Build Dockerfile template: %s", err) + } + templateData := JbossDockerfileTemplate{} + warConfig := artifacts.WarArtifactConfig{} + if err := newArtifact.GetConfig(artifacts.WarConfigType, &warConfig); err == nil { + // WAR + javaPackage, err := getJavaPackage(filepath.Join(t.Env.GetEnvironmentContext(), versionMappingFilePath), warConfig.JavaVersion) + if err != nil { + logrus.Errorf("Unable to find mapping version for java version %s : %s", warConfig.JavaVersion, err) + javaPackage = defaultJavaPackage + } + templateData.JavaPackageName = javaPackage + templateData.DeploymentFilePath = warConfig.DeploymentFilePath + templateData.Port = defaultJbossPort + templateData.EnvVariables = warConfig.EnvVariables + templateData.BuildContainerName = warConfig.BuildContainerName + } else { + // EAR + logrus.Debugf("unable to load config for Transformer into %T : %s", warConfig, err) + earConfig := artifacts.EarArtifactConfig{} + if err := newArtifact.GetConfig(artifacts.EarConfigType, &earConfig); err != nil { + logrus.Debugf("unable to load config for Transformer into %T : %s", earConfig, err) + } + javaPackage, err := getJavaPackage(filepath.Join(t.Env.GetEnvironmentContext(), versionMappingFilePath), earConfig.JavaVersion) + if err != nil { + logrus.Errorf("Unable to find mapping version for java version %s : %s", earConfig.JavaVersion, err) + javaPackage = defaultJavaPackage + } + templateData.JavaPackageName = javaPackage + templateData.DeploymentFilePath = earConfig.DeploymentFilePath + templateData.Port = defaultJbossPort + templateData.EnvVariables = earConfig.EnvVariables + templateData.BuildContainerName = earConfig.BuildContainerName + } + pathMappings = append(pathMappings, transformertypes.PathMapping{ + Type: transformertypes.SourcePathMappingType, + DestPath: common.DefaultSourceDir, + }) + pathMappings = append(pathMappings, transformertypes.PathMapping{ + Type: transformertypes.TemplatePathMappingType, + SrcPath: dockerfileTemplatePath, + DestPath: filepath.Join(common.DefaultSourceDir, relServiceDir), + TemplateConfig: templateData, + }) + paths := newArtifact.Paths + paths[artifacts.DockerfilePathType] = []string{filepath.Join(common.DefaultSourceDir, relServiceDir, common.DefaultDockerfileName)} + dockerfileArtifact := transformertypes.Artifact{ + Name: imageName.ImageName, + Type: artifacts.DockerfileArtifactType, + Paths: paths, + Configs: map[transformertypes.ConfigType]interface{}{ + artifacts.ImageNameConfigType: imageName, + }, + } + dockerfileServiceArtifact := transformertypes.Artifact{ + Name: serviceConfig.ServiceName, + Type: artifacts.DockerfileForServiceArtifactType, + Paths: newArtifact.Paths, + Configs: map[transformertypes.ConfigType]interface{}{ + artifacts.ImageNameConfigType: imageName, + artifacts.ServiceConfigType: serviceConfig, + }, + } + //TODO: WASI + //ir := irtypes.IR{} + //if err = newArtifact.GetConfig(irtypes.IRConfigType, &ir); err == nil { + // dockerfileServiceArtifact.Configs[irtypes.IRConfigType] = ir + //} + createdArtifacts = append(createdArtifacts, dockerfileArtifact, dockerfileServiceArtifact) + } + return pathMappings, createdArtifacts, nil +} + +func (t *Jboss) getDockerfileTemplate(newArtifact transformertypes.Artifact) (string, error) { + jbossRunTemplatePath := filepath.Join(t.Env.GetEnvironmentContext(), t.Env.RelTemplatesDir, "Dockerfile.jboss") + jbossRunTemplate, err := os.ReadFile(jbossRunTemplatePath) + if err != nil { + return "", fmt.Errorf("failed to read the jboss run stage Dockerfile template at path %s . Error: %q", jbossRunTemplatePath, err) + } + dockerFileHead := "" + if buildContainerPaths := newArtifact.Paths[artifacts.BuildContainerFileType]; len(buildContainerPaths) > 0 { + dockerfileBuildPath := buildContainerPaths[0] + dockerFileHeadBytes, err := os.ReadFile(dockerfileBuildPath) + if err != nil { + return "", fmt.Errorf("failed to read the build stage Dockerfile template at path %s . Error: %q", dockerfileBuildPath, err) + } + dockerFileHead = string(dockerFileHeadBytes) + } else { + licenseFilePath := filepath.Join(t.Env.GetEnvironmentContext(), t.Env.RelTemplatesDir, "Dockerfile.license") + dockerFileHeadBytes, err := os.ReadFile(licenseFilePath) + if err != nil { + return "", fmt.Errorf("failed to read the Dockerfile license at path %s . Error: %q", licenseFilePath, err) + } + dockerFileHead = string(dockerFileHeadBytes) + } + return dockerFileHead + "\n" + string(jbossRunTemplate), nil +} diff --git a/transformer/dockerfilegenerator/java/liberty.go b/transformer/dockerfilegenerator/java/liberty.go new file mode 100644 index 0000000..0ac03ee --- /dev/null +++ b/transformer/dockerfilegenerator/java/liberty.go @@ -0,0 +1,248 @@ +/* + * Copyright IBM Corporation 2021 + * + * 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 java + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/konveyor/move2kube-wasm/common" + "github.com/konveyor/move2kube-wasm/environment" + "github.com/konveyor/move2kube-wasm/types" + //irtypes "github.com/konveyor/move2kube-wasm/types/ir" + transformertypes "github.com/konveyor/move2kube-wasm/types/transformer" + "github.com/konveyor/move2kube-wasm/types/transformer/artifacts" + "github.com/sirupsen/logrus" +) + +const ( + defaultLibertyPort int32 = 9080 +) + +// Liberty implements Transformer interface +type Liberty struct { + Config transformertypes.Transformer + Env *environment.Environment + LibertyConfig *LibertyYamlConfig +} + +// LibertyYamlConfig stores jar related configuration information +type LibertyYamlConfig struct { + JavaVersion string `yaml:"defaultJavaVersion"` +} + +// LibertyDockerfileTemplate stores parameters for the dockerfile template +type LibertyDockerfileTemplate struct { + JavaPackageName string + JavaVersion string + DeploymentFilePath string + BuildContainerName string + Port int32 + EnvVariables map[string]string +} + +// JavaLibertyImageMapping stores the java version to liberty image version mappings +type JavaLibertyImageMapping struct { + types.TypeMeta `yaml:",inline"` + types.ObjectMeta `yaml:"metadata,omitempty"` + Spec JavaLibertyImageMappingSpec `yaml:"spec,omitempty"` +} + +// JavaLibertyImageMappingSpec stores the java to liberty image version spec +type JavaLibertyImageMappingSpec struct { + Mapping map[string]string `yaml:"mapping"` +} + +// Init Initializes the transformer +func (t *Liberty) Init(tc transformertypes.Transformer, env *environment.Environment) (err error) { + t.Config = tc + t.Env = env + t.LibertyConfig = &LibertyYamlConfig{} + err = common.GetObjFromInterface(t.Config.Spec.Config, t.LibertyConfig) + if err != nil { + logrus.Errorf("unable to load config for Transformer %+v into %T : %s", t.Config.Spec.Config, t.LibertyConfig, err) + return err + } + // defaults + if t.LibertyConfig.JavaVersion == "" { + t.LibertyConfig.JavaVersion = defaultJavaVersion + } + return nil +} + +// GetConfig returns the transformer config +func (t *Liberty) GetConfig() (transformertypes.Transformer, *environment.Environment) { + return t.Config, t.Env +} + +// DirectoryDetect runs detect in each sub directory +func (t *Liberty) DirectoryDetect(dir string) (services map[string][]transformertypes.Artifact, err error) { + return +} + +// Transform transforms the artifacts +func (t *Liberty) Transform(newArtifacts []transformertypes.Artifact, alreadySeenArtifacts []transformertypes.Artifact) ([]transformertypes.PathMapping, []transformertypes.Artifact, error) { + pathMappings := []transformertypes.PathMapping{} + createdArtifacts := []transformertypes.Artifact{} + for _, newArtifact := range newArtifacts { + if newArtifact.Type != artifacts.WarArtifactType { + continue + } + serviceConfig := artifacts.ServiceConfig{} + if err := newArtifact.GetConfig(artifacts.ServiceConfigType, &serviceConfig); err != nil { + logrus.Errorf("failed to load service config from the artifact: %+v . Error: %q", serviceConfig, err) + continue + } + if serviceConfig.ServiceName == "" { + serviceConfig.ServiceName = common.MakeStringK8sServiceNameCompliant(newArtifact.Name) + } + imageName := artifacts.ImageName{} + if err := newArtifact.GetConfig(artifacts.ImageNameConfigType, &imageName); err != nil { + logrus.Debugf("unable to load config for Transformer into %T : %s", imageName, err) + } + if imageName.ImageName == "" { + imageName.ImageName = common.MakeStringContainerImageNameCompliant(newArtifact.Name) + } + if len(newArtifact.Paths[artifacts.ServiceDirPathType]) == 0 { + logrus.Errorf("service directory missing from artifact: %+v", newArtifact) + continue + } + serviceDir := newArtifact.Paths[artifacts.ServiceDirPathType][0] + relServiceDir, err := filepath.Rel(t.Env.GetEnvironmentSource(), serviceDir) + if err != nil { + logrus.Errorf("failed to make the source path %s relative to the source code directory %s . Error: %q", serviceDir, t.Env.GetEnvironmentSource(), err) + continue + } + template, err := t.getDockerfileTemplate(newArtifact) + if err != nil { + logrus.Errorf("failed to get the liberty run stage Dockerfile template. Error: %q", err) + continue + } + tempDir := filepath.Join(t.Env.TempPath, newArtifact.Name) + if err := os.MkdirAll(tempDir, common.DefaultDirectoryPermission); err != nil { + logrus.Errorf("failed to make the temporary directory %s . Error: %q", tempDir, err) + continue + } + dockerfileTemplatePath := filepath.Join(tempDir, common.DefaultDockerfileName) + if err := os.WriteFile(dockerfileTemplatePath, []byte(template), common.DefaultFilePermission); err != nil { + logrus.Errorf("failed to write the liberty Dockerfile template to the temporary file at path %s . Error: %q", dockerfileTemplatePath, err) + continue + } + templateData := LibertyDockerfileTemplate{} + warConfig := artifacts.WarArtifactConfig{} + if err := newArtifact.GetConfig(artifacts.WarConfigType, &warConfig); err == nil { + // WAR + if warConfig.JavaVersion == "" { + warConfig.JavaVersion = t.LibertyConfig.JavaVersion + } + javaPackage, err := getJavaPackage(filepath.Join(t.Env.GetEnvironmentContext(), versionMappingFilePath), warConfig.JavaVersion) + if err != nil { + logrus.Errorf("Unable to find mapping version for java version %s . Error: %q", warConfig.JavaVersion, err) + javaPackage = defaultJavaPackage + } + templateData.JavaPackageName = javaPackage + templateData.JavaVersion = warConfig.JavaVersion + templateData.Port = defaultLibertyPort + templateData.EnvVariables = warConfig.EnvVariables + templateData.DeploymentFilePath = warConfig.DeploymentFilePath + templateData.BuildContainerName = warConfig.BuildContainerName + } else { + // EAR + logrus.Debugf("failed to load war config from the artifact: %+v . Error: %q", newArtifact, err) + earConfig := artifacts.EarArtifactConfig{} + if err := newArtifact.GetConfig(artifacts.EarConfigType, &earConfig); err != nil { + logrus.Debugf("failed to load ear config from the artifact: %+v . Error: %q", newArtifact, err) + } + if earConfig.JavaVersion == "" { + earConfig.JavaVersion = t.LibertyConfig.JavaVersion + } + javaPackage, err := getJavaPackage(filepath.Join(t.Env.GetEnvironmentContext(), versionMappingFilePath), earConfig.JavaVersion) + if err != nil { + logrus.Errorf("Unable to find mapping version for java version %s : %s", earConfig.JavaVersion, err) + javaPackage = defaultJavaPackage + } + templateData.JavaPackageName = javaPackage + templateData.JavaVersion = earConfig.JavaVersion + templateData.Port = defaultLibertyPort + templateData.EnvVariables = earConfig.EnvVariables + templateData.DeploymentFilePath = earConfig.DeploymentFilePath + templateData.BuildContainerName = earConfig.BuildContainerName + } + pathMappings = append(pathMappings, transformertypes.PathMapping{ + Type: transformertypes.SourcePathMappingType, + DestPath: common.DefaultSourceDir, + }) + pathMappings = append(pathMappings, transformertypes.PathMapping{ + Type: transformertypes.TemplatePathMappingType, + SrcPath: dockerfileTemplatePath, + DestPath: filepath.Join(common.DefaultSourceDir, relServiceDir), + TemplateConfig: templateData, + }) + paths := newArtifact.Paths + paths[artifacts.DockerfilePathType] = []string{filepath.Join(common.DefaultSourceDir, relServiceDir, common.DefaultDockerfileName)} + dockerfileArtifact := transformertypes.Artifact{ + Name: imageName.ImageName, + Type: artifacts.DockerfileArtifactType, + Paths: paths, + Configs: map[transformertypes.ConfigType]interface{}{ + artifacts.ImageNameConfigType: imageName, + }, + } + dockerfileServiceAritfact := transformertypes.Artifact{ + Name: serviceConfig.ServiceName, + Type: artifacts.DockerfileForServiceArtifactType, + Paths: newArtifact.Paths, + Configs: map[transformertypes.ConfigType]interface{}{ + artifacts.ImageNameConfigType: imageName, + artifacts.ServiceConfigType: serviceConfig, + }, + } + //TODO: WASI + //ir := irtypes.IR{} + //if err := newArtifact.GetConfig(irtypes.IRConfigType, &ir); err == nil { + // dockerfileServiceAritfact.Configs[irtypes.IRConfigType] = ir + //} + createdArtifacts = append(createdArtifacts, dockerfileArtifact, dockerfileServiceAritfact) + } + return pathMappings, createdArtifacts, nil +} + +func (t *Liberty) getDockerfileTemplate(newArtifact transformertypes.Artifact) (string, error) { + libertyRunTemplatePath := filepath.Join(t.Env.GetEnvironmentContext(), t.Env.RelTemplatesDir, "Dockerfile.liberty") + libertyRunTemplate, err := os.ReadFile(libertyRunTemplatePath) + if err != nil { + return "", fmt.Errorf("failed to read the liberty run stage Dockerfile template at path %s . Error: %q", libertyRunTemplatePath, err) + } + dockerFileHead := "" + if buildContainerPaths := newArtifact.Paths[artifacts.BuildContainerFileType]; len(buildContainerPaths) > 0 { + dockerfileBuildPath := buildContainerPaths[0] + dockerFileHeadBytes, err := os.ReadFile(dockerfileBuildPath) + if err != nil { + return "", fmt.Errorf("failed to read the build stage Dockerfile template at path %s . Error: %q", dockerfileBuildPath, err) + } + dockerFileHead = string(dockerFileHeadBytes) + } else { + licenseFilePath := filepath.Join(t.Env.GetEnvironmentContext(), t.Env.RelTemplatesDir, "Dockerfile.license") + dockerFileHeadBytes, err := os.ReadFile(licenseFilePath) + if err != nil { + return "", fmt.Errorf("failed to read the Dockerfile license at path %s . Error: %q", licenseFilePath, err) + } + dockerFileHead = string(dockerFileHeadBytes) + } + return dockerFileHead + "\n" + string(libertyRunTemplate), nil +} diff --git a/transformer/dockerfilegenerator/java/mavenanalyser.go b/transformer/dockerfilegenerator/java/mavenanalyser.go new file mode 100644 index 0000000..e4538ea --- /dev/null +++ b/transformer/dockerfilegenerator/java/mavenanalyser.go @@ -0,0 +1,732 @@ +/* + * Copyright IBM Corporation 2021 + * + * 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 java + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/konveyor/move2kube-wasm/common" + "github.com/konveyor/move2kube-wasm/environment" + //"github.com/konveyor/move2kube-wasm/qaengine" + //irtypes "github.com/konveyor/move2kube-wasm/types/ir" + //"github.com/konveyor/move2kube-wasm/types/qaengine/commonqa" + "github.com/konveyor/move2kube-wasm/types/source/maven" + transformertypes "github.com/konveyor/move2kube-wasm/types/transformer" + "github.com/konveyor/move2kube-wasm/types/transformer/artifacts" + "github.com/sirupsen/logrus" + //"github.com/spf13/cast" +) + +const ( + defaultMavenVersion = "3.8.4" + // MAVEN_COMPILER_PLUGIN is the name of the maven plugin that compiles the java code. + MAVEN_COMPILER_PLUGIN = "maven-compiler-plugin" + // MAVEN_JAR_PLUGIN is the name of the maven plugin that packages the java code. + MAVEN_JAR_PLUGIN = "maven-jar-plugin" + // SPRING_BOOT_MAVEN_PLUGIN is the name of the maven plugin that Spring Boot uses. + SPRING_BOOT_MAVEN_PLUGIN = "spring-boot-maven-plugin" + // MAVEN_DEFAULT_BUILD_DIR is the name of the default build directory + MAVEN_DEFAULT_BUILD_DIR = "target" +) + +// MavenAnalyser implements Transformer interface +type MavenAnalyser struct { + Config transformertypes.Transformer + Env *environment.Environment + MavenConfig *MavenYamlConfig +} + +// MavenYamlConfig stores the maven related information +type MavenYamlConfig struct { + MavenVersion string `yaml:"defaultMavenVersion"` + JavaVersion string `yaml:"defaultJavaVersion"` + AppPathInBuildContainer string `yaml:"appPathInBuildContainer"` +} + +// MavenBuildDockerfileTemplate defines the information for the build dockerfile template +type MavenBuildDockerfileTemplate struct { + MvnwPresent bool + IsParentPom bool + JavaPackageName string + MavenVersion string + BuildContainerName string + MavenProfiles []string + EnvVariables map[string]string +} + +// Init initializes the transformer +func (t *MavenAnalyser) Init(tc transformertypes.Transformer, env *environment.Environment) (err error) { + t.Config = tc + t.Env = env + t.MavenConfig = &MavenYamlConfig{} + err = common.GetObjFromInterface(t.Config.Spec.Config, t.MavenConfig) + if err != nil { + logrus.Errorf("unable to load config for Transformer %+v into %T : %s", t.Config.Spec.Config, t.MavenConfig, err) + return err + } + if t.MavenConfig.MavenVersion == "" { + t.MavenConfig.MavenVersion = defaultMavenVersion + } + if t.MavenConfig.JavaVersion == "" { + t.MavenConfig.JavaVersion = defaultJavaVersion + } + if t.MavenConfig.AppPathInBuildContainer == "" { + t.MavenConfig.AppPathInBuildContainer = defaultAppPathInContainer + } + return nil +} + +// GetConfig returns the transformer config +func (t *MavenAnalyser) GetConfig() (transformertypes.Transformer, *environment.Environment) { + return t.Config, t.Env +} + +// DirectoryDetect runs detect in each sub directory +func (t *MavenAnalyser) DirectoryDetect(dir string) (map[string][]transformertypes.Artifact, error) { + + // look for pom.xml + + // There will be at most one file path because GetFilesInCurrentDirectory does not check subdirectories. + mavenFilePaths, err := common.GetFilesInCurrentDirectory(dir, []string{maven.PomXMLFileName}, nil) + if err != nil { + return nil, fmt.Errorf("failed to look for maven %s files in the directory %s . Error: %q", maven.PomXMLFileName, dir, err) + } + + // if pom.xml is missing then skip + + if len(mavenFilePaths) == 0 { + return nil, nil + } + + // start filling in the paths for the service artifact + + paths := map[transformertypes.PathType][]string{ + artifacts.ServiceRootDirPathType: {dir}, + artifacts.ServiceDirPathType: {dir}, + artifacts.MavenPomPathType: mavenFilePaths, + } + + // start filling in the maven config object for the service artifact + + mavenConfig := artifacts.MavenConfig{} + + // check if maven wrapper script is present + + if mvnwFilePaths, err := common.GetFilesInCurrentDirectory(dir, []string{"mvnw"}, nil); err == nil && len(mvnwFilePaths) > 0 { + mavenConfig.IsMvnwPresent = true + } + + // parse the pom.xml to get the name and child modules of the root project + + pom := &maven.Pom{} + pomFilePath := mavenFilePaths[0] + if err := pom.Load(pomFilePath); err != nil { + return nil, fmt.Errorf("failed to parse the pom.xml file at path %s . Error: %q", pomFilePath, err) + } + mavenConfig.MavenAppName = pom.ArtifactID + + // if the artifact id is empty in the root pom.xml then by default the directory name is used + + if mavenConfig.MavenAppName == "" { + mavenConfig.MavenAppName = filepath.Base(dir) + } + + // Get the paths to each of the child module directories (relative to the root project directory). + // These directories will be ignored during the rest of the planning (directory detect) phase. + // Also get the packaging type (jar/war/ear) from the root pom.xml. + + mavenConfig.PackagingType = artifacts.JavaPackaging(pom.Packaging) + mavenConfig.ChildModules = []artifacts.ChildModule{{Name: mavenConfig.MavenAppName, RelPomPath: maven.PomXMLFileName}} + if isParentPom(pom) { + if pom.Modules == nil || len(*pom.Modules) == 0 { + return nil, fmt.Errorf("the list of child modules is empty for the parent pom.xml at path %s", pomFilePath) + } + mavenConfig.PackagingType = artifacts.PomPackaging + mavenConfig.ChildModules = []artifacts.ChildModule{} + paths[artifacts.ServiceDirPathType] = []string{} + for _, relChildModulePomPath := range *pom.Modules { + relChildModulePomPath = filepath.Clean(relChildModulePomPath) + if filepath.Ext(relChildModulePomPath) != ".xml" { + relChildModulePomPath = filepath.Join(relChildModulePomPath, maven.PomXMLFileName) + } + + // parse the child module pom.xml to get the artifact id + + childModulePomPath := filepath.Join(dir, relChildModulePomPath) + childModulePom := &maven.Pom{} + if err := childModulePom.Load(childModulePomPath); err != nil { + logrus.Errorf("failed to load the child module pom.xml file at path %s Error: %q", childModulePomPath, err) + continue + } + mavenConfig.ChildModules = append(mavenConfig.ChildModules, artifacts.ChildModule{Name: childModulePom.ArtifactID, RelPomPath: relChildModulePomPath}) + paths[artifacts.ServiceDirPathType] = append(paths[artifacts.ServiceDirPathType], filepath.Dir(childModulePomPath)) + } + } + if mavenConfig.PackagingType == "" { + mavenConfig.PackagingType = artifacts.JarPackaging + } + + // find all the maven profiles + + mavenConfig.MavenProfiles = []string{} + if pom.Profiles != nil { + for _, profile := range *pom.Profiles { + mavenConfig.MavenProfiles = append(mavenConfig.MavenProfiles, profile.ID) + } + } + + // create the service artifact + + serviceArtifact := transformertypes.Artifact{ + Paths: paths, + Configs: map[transformertypes.ConfigType]interface{}{ + artifacts.MavenConfigType: mavenConfig, + }, + } + normalizedServiceName := common.MakeStringK8sServiceNameCompliant(mavenConfig.MavenAppName) + services := map[string][]transformertypes.Artifact{normalizedServiceName: {serviceArtifact}} + + return services, nil +} + +// Transform transforms the input artifacts mostly handling artifacts created during the plan phase. +func (t *MavenAnalyser) Transform(newArtifacts []transformertypes.Artifact, alreadySeenArtifacts []transformertypes.Artifact) ([]transformertypes.PathMapping, []transformertypes.Artifact, error) { + pathMappings := []transformertypes.PathMapping{} + createdArtifacts := []transformertypes.Artifact{} + for _, newArtifact := range newArtifacts { + if newArtifact.Type != artifacts.ServiceArtifactType { + continue + } + serviceConfig := artifacts.ServiceConfig{} + if err := newArtifact.GetConfig(artifacts.ServiceConfigType, &serviceConfig); err != nil { + logrus.Debugf("unable to load config for Transformer into %T : %s", serviceConfig, err) + continue + } + mavenConfig := artifacts.MavenConfig{} + if err := newArtifact.GetConfig(artifacts.MavenConfigType, &mavenConfig); err != nil { + continue + } + mavenPomPaths := newArtifact.Paths[artifacts.MavenPomPathType] + if len(mavenPomPaths) == 0 { + logrus.Errorf("the artifact doesn't contain any maven pom.xml paths. Artifact: %+v", newArtifact) + continue + } + pom := &maven.Pom{} + rootPomFilePath := mavenPomPaths[0] // In a multi-module project this is just the parent pom.xml + if err := pom.Load(rootPomFilePath); err != nil { + logrus.Errorf("failed to load the pom.xml file at path %s . Error: %q", rootPomFilePath, err) + continue + } + currPathMappings, currArtifacts, err := t.TransformArtifact(newArtifact, alreadySeenArtifacts, pom, rootPomFilePath, serviceConfig, mavenConfig) + if err != nil { + logrus.Errorf("failed to transform the artifact: %+v . Error: %q", newArtifact, err) + continue + } + pathMappings = append(pathMappings, currPathMappings...) + createdArtifacts = append(createdArtifacts, currArtifacts...) + } + return pathMappings, createdArtifacts, nil +} + +type infoT struct { + Name string + Type transformertypes.ArtifactType + IsParentPom bool + IsMvnwPresent bool + JavaVersion string + DeploymentFilePath string + MavenProfiles []string + ChildModules []artifacts.ChildModule + SpringBoot *artifacts.SpringBootConfig +} + +// TransformArtifact is the same as Transform but operating on a single artifact and its pom.xml at a time. +func (t *MavenAnalyser) TransformArtifact(newArtifact transformertypes.Artifact, alreadySeenArtifacts []transformertypes.Artifact, pom *maven.Pom, rootPomFilePath string, serviceConfig artifacts.ServiceConfig, mavenConfig artifacts.MavenConfig) ([]transformertypes.PathMapping, []transformertypes.Artifact, error) { + pathMappings := []transformertypes.PathMapping{} + createdArtifacts := []transformertypes.Artifact{} + + selectedBuildOption, err := askUserForDockerfileType(serviceConfig.ServiceName) + if err != nil { + return pathMappings, createdArtifacts, err + } + logrus.Debugf("user chose to generate Dockefiles that have '%s'", selectedBuildOption) + + // ask the user which child modules should be run in the K8s cluster + + selectedChildModuleNames := []string{} + for _, childModule := range mavenConfig.ChildModules { + selectedChildModuleNames = append(selectedChildModuleNames, childModule.Name) + } + if len(selectedChildModuleNames) > 1 { + //TODO: WASI + //quesKey := fmt.Sprintf(common.ConfigServicesChildModulesNamesKey, `"`+serviceConfig.ServiceName+`"`) + //desc := fmt.Sprintf("For the multi-module Maven project '%s', please select all the child modules that should be run as services in the cluster:", serviceConfig.ServiceName) + //hints := []string{"deselect child modules that should not be run (like libraries)"} + //selectedChildModuleNames = qaengine.FetchMultiSelectAnswer(quesKey, desc, hints, selectedChildModuleNames, selectedChildModuleNames, nil) + if len(selectedChildModuleNames) == 0 { + return pathMappings, createdArtifacts, fmt.Errorf("user deselected all the child modules of the maven multi-module project '%s'", serviceConfig.ServiceName) + } + } + + // have jar/war/ear analyzer transformers generate a Dockerfile with only the run stage for each of the child modules + + lowestJavaVersion := "" + imageToCopyFrom := serviceConfig.ServiceName + "-" + buildStageC + serviceRootDir := newArtifact.Paths[artifacts.ServiceRootDirPathType][0] + + for _, childModule := range mavenConfig.ChildModules { + + // only look at the child modules the user selected + + if !common.IsPresent(selectedChildModuleNames, childModule.Name) { + continue + } + + // parse the pom.xml of this child module + + childPomFilePath := filepath.Join(serviceRootDir, childModule.RelPomPath) + childPom := &maven.Pom{} + if err := childPom.Load(childPomFilePath); err != nil { + logrus.Errorf("failed to load the child pom.xml at path %s . Error: %q", childPomFilePath, err) + continue + } + + // get some info about the child module to fill the artifact + + childModuleInfo, err := getInfoFromPom(childPom, pom, childPomFilePath, nil) + if err != nil { + logrus.Errorf("failed to get information from the child pom %+v . Error: %q", childPom, err) + continue + } + + // Find the lowest java version among all of the child modules. + // We will use this java version while doing the build. + + if lowestJavaVersion == "" { + lowestJavaVersion = childModuleInfo.JavaVersion + } + + // have the user select which spring boot profiles to use and find a suitable list of ports + //TODO: WASI + //desc := fmt.Sprintf("Select the spring boot profiles for the service '%s' :", childModule.Name) + //hints := []string{"select all the profiles that are applicable"} + detectedPorts := []int32{} + envVarsMap := map[string]string{} + if childModuleInfo.SpringBoot != nil { + if childModuleInfo.SpringBoot.SpringBootProfiles != nil && len(*childModuleInfo.SpringBoot.SpringBootProfiles) != 0 { + //quesKey := fmt.Sprintf(common.ConfigServicesChildModulesSpringProfilesKey, `"`+serviceConfig.ServiceName+`"`, `"`+childModule.Name+`"`) + //selectedSpringProfiles := qaengine.FetchMultiSelectAnswer(quesKey, desc, hints, *childModuleInfo.SpringBoot.SpringBootProfiles, *childModuleInfo.SpringBoot.SpringBootProfiles, nil) + selectedSpringProfiles := []string{} + for _, selectedSpringProfile := range selectedSpringProfiles { + detectedPorts = append(detectedPorts, childModuleInfo.SpringBoot.SpringBootProfilePorts[selectedSpringProfile]...) + } + envVarsMap["SPRING_PROFILES_ACTIVE"] = strings.Join(selectedSpringProfiles, ",") + } else { + detectedPorts = childModuleInfo.SpringBoot.SpringBootProfilePorts[defaultSpringProfile] + } + } + + // have the user select the port to use + + //TODO: WASI + selectedPort := int32(9002) + //selectedPort := commonqa.GetPortForService(detectedPorts, common.JoinQASubKeys(`"`+serviceConfig.ServiceName+`"`, "childModules", `"`+childModule.Name+`"`)) + //if childModuleInfo.SpringBoot != nil { + // envVarsMap["SERVER_PORT"] = cast.ToString(selectedPort) + //} else { + // envVarsMap["PORT"] = cast.ToString(selectedPort) + //} + + // find the path to the artifact (jar/war/ear) which should get copied into the run stage + + relDeploymentFilePath, err := filepath.Rel(serviceRootDir, childModuleInfo.DeploymentFilePath) + if err != nil { + logrus.Errorf("failed to make the path %s relative to the service directory %s . Error: %q", childModuleInfo.DeploymentFilePath, serviceRootDir, err) + continue + } + insideContainerDepFilePath := filepath.Join(t.MavenConfig.AppPathInBuildContainer, relDeploymentFilePath) + childModuleDir := filepath.Dir(childPomFilePath) + if selectedBuildOption == NO_BUILD_STAGE { + imageToCopyFrom = "" + relDeploymentFilePath, err = filepath.Rel(childModuleDir, childModuleInfo.DeploymentFilePath) + if err != nil { + logrus.Errorf("failed to make the jar/war/ear archive path %s relative to the child module service directory %s . Error: %q", childModuleInfo.DeploymentFilePath, childModuleDir, err) + continue + } + insideContainerDepFilePath = relDeploymentFilePath + } + + // create an artifact that will get picked up by the jar/war/ear analyzer transformers + + runStageArtifact := transformertypes.Artifact{ + Name: childModule.Name, + Type: childModuleInfo.Type, + Paths: map[transformertypes.PathType][]string{artifacts.ServiceDirPathType: {filepath.Dir(childPomFilePath)}}, + Configs: map[transformertypes.ConfigType]interface{}{ + transformertypes.ConfigType(childModuleInfo.Type): artifacts.JarArtifactConfig{ + Port: selectedPort, + JavaVersion: childModuleInfo.JavaVersion, + BuildContainerName: imageToCopyFrom, + DeploymentFilePath: insideContainerDepFilePath, + EnvVariables: envVarsMap, + }, + artifacts.ImageNameConfigType: artifacts.ImageName{ImageName: common.MakeStringContainerImageNameCompliant(childModule.Name)}, + artifacts.ServiceConfigType: artifacts.ServiceConfig{ServiceName: common.MakeStringK8sServiceNameCompliant(childModule.Name)}, + }, + } + + // preserve the ir config and inject cloud foundry vcap properties if it is present + + //TODO: WASI + //ir := irtypes.IR{} + //if err := newArtifact.GetConfig(irtypes.IRConfigType, &ir); err == nil { + // ir = injectProperties(ir, childModuleInfo.Name) + // runStageArtifact.Configs[irtypes.IRConfigType] = ir + //} + + createdArtifacts = append(createdArtifacts, runStageArtifact) + } + + if selectedBuildOption == NO_BUILD_STAGE { + return pathMappings, createdArtifacts, nil + } + + // Find the java package corresponding to the java version. + // This will be installed inside the build stage Dockerfile. + + if lowestJavaVersion == "" { + lowestJavaVersion = t.MavenConfig.JavaVersion + } + javaPackageName, err := t.getJavaPackage(lowestJavaVersion) + if err != nil { + return pathMappings, createdArtifacts, fmt.Errorf("failed to get the java package for the java version %s . Error: %q", lowestJavaVersion, err) + } + + // write the build stage Dockerfile template to a temporary file for the pathmapping to pick it up + + dockerfileTemplate, err := t.getDockerfileTemplate() + if err != nil { + return pathMappings, createdArtifacts, fmt.Errorf("failed to get the Dockerfile template. Error: %q", err) + } + tempDir, err := os.MkdirTemp(t.Env.TempPath, "maven-transformer-build-*") + if err != nil { + return pathMappings, createdArtifacts, fmt.Errorf("failed to create a temporary directory inside the directory %s . Error: %q", t.Env.TempPath, err) + } + dockerfileTemplatePath := filepath.Join(tempDir, common.DefaultDockerfileName+".build.template") + if err := os.WriteFile(dockerfileTemplatePath, []byte(dockerfileTemplate), common.DefaultFilePermission); err != nil { + return pathMappings, createdArtifacts, fmt.Errorf("failed to write the Dockerfile template to a temporary file at path %s . Error: %q", dockerfileTemplatePath, err) + } + + // the build stage Dockefile should be placed in the root project directory + + relServiceRootDir, err := filepath.Rel(t.Env.GetEnvironmentSource(), serviceRootDir) + if err != nil { + return pathMappings, createdArtifacts, fmt.Errorf("failed to make the service directory %s relative to the source code directory %s . Error: %q", serviceRootDir, t.Env.GetEnvironmentSource(), err) + } + dockerfilePath := filepath.Join(common.DefaultSourceDir, relServiceRootDir, common.DefaultDockerfileName+"."+buildStageC) + + // collect data to fill the Dockerfile template + + rootPomInfo, err := getInfoFromPom(pom, nil, rootPomFilePath, &mavenConfig) + if err != nil { + return pathMappings, createdArtifacts, fmt.Errorf("failed to get the info from the pom.xml at path %s . Error: %q", rootPomFilePath, err) + } + + // ask the user which maven profiles should be used while building the app + + //TODO: WASI + //selectedMavenProfiles := qaengine.FetchMultiSelectAnswer( + // common.JoinQASubKeys(common.ConfigServicesKey, `"`+serviceConfig.ServiceName+`"`, "mavenProfiles"), + // fmt.Sprintf("Select the maven profiles to use for the '%s' service", serviceConfig.ServiceName), + // []string{"The selected maven profiles will be used during the build."}, + // rootPomInfo.MavenProfiles, + // rootPomInfo.MavenProfiles, + // nil, + //) + selectedMavenProfiles := []string{} + + // fill in the Dockerfile template for the build stage and write it out using a pathmapping + + buildStageDockerfilePathMapping := transformertypes.PathMapping{ + Type: transformertypes.TemplatePathMappingType, + SrcPath: dockerfileTemplatePath, + DestPath: dockerfilePath, + TemplateConfig: MavenBuildDockerfileTemplate{ + MvnwPresent: rootPomInfo.IsMvnwPresent, + IsParentPom: rootPomInfo.IsParentPom, + JavaPackageName: javaPackageName, + MavenVersion: t.MavenConfig.MavenVersion, + BuildContainerName: imageToCopyFrom, + MavenProfiles: selectedMavenProfiles, + EnvVariables: map[string]string{}, // TODO: Something about getting env vars from the IR config inside the artifact coming from the cloud foundry transformer? + }, + } + + if selectedBuildOption == BUILD_IN_EVERY_IMAGE { + dockerfilePath = filepath.Join(tempDir, common.DefaultDockerfileName+"."+buildStageC) + buildStageDockerfilePathMapping.DestPath = dockerfilePath + for _, createdArtifact := range createdArtifacts { + createdArtifact.Paths[artifacts.BuildContainerFileType] = []string{dockerfilePath} + } + } else { + + // make sure the source code directory has been copied over first + + copySourceDirPathMapping := transformertypes.PathMapping{ + Type: transformertypes.SourcePathMappingType, + DestPath: common.DefaultSourceDir, + } + pathMappings = append(pathMappings, copySourceDirPathMapping) + + // Tell the other transformers about the build stage Dockerfile we created. + // That way, the image will get built by the builddockerimages.sh script. + + baseImageDockerfileArtifact := transformertypes.Artifact{ + Name: imageToCopyFrom, + Type: artifacts.DockerfileArtifactType, + Paths: map[transformertypes.PathType][]string{artifacts.DockerfilePathType: {dockerfilePath}}, // TODO: should we add the context path as well? + Configs: map[transformertypes.ConfigType]interface{}{artifacts.ImageNameConfigType: artifacts.ImageName{ImageName: imageToCopyFrom}}, + } + createdArtifacts = append(createdArtifacts, baseImageDockerfileArtifact) + } + pathMappings = append(pathMappings, buildStageDockerfilePathMapping) + + return pathMappings, createdArtifacts, nil +} + +func getInfoFromPom(pom, parentPom *maven.Pom, pomFilePath string, mavenConfig *artifacts.MavenConfig) (infoT, error) { + name := pom.ArtifactID + if mavenConfig != nil { + name = mavenConfig.MavenAppName + } + packaging := artifacts.JavaPackaging(pom.Packaging) + if mavenConfig != nil { + packaging = mavenConfig.PackagingType + } + artifactType, err := packagingToArtifactType(packaging) + if err != nil && string(artifactType) != pom.Packaging { + logrus.Warnf("failed to convert the packaging type '%s' to an artifact type. Error: %q", packaging, err) + } + if artifactType == "" { + artifactType = artifacts.JarArtifactType + } + isParentPom := isParentPom(pom) + pomFileDir := filepath.Dir(pomFilePath) + isMvnwPresent := false + if mavenConfig != nil { + isMvnwPresent = mavenConfig.IsMvnwPresent + } else { + if mvnwFilePaths, err := common.GetFilesInCurrentDirectory(pomFileDir, []string{"mvnw"}, nil); err == nil && len(mvnwFilePaths) > 0 { + isMvnwPresent = true + } + } + javaVersion := getJavaVersionFromPom(pom) + mavenProfiles := []string{} + if pom.Profiles != nil { + for _, profile := range *pom.Profiles { + mavenProfiles = append(mavenProfiles, profile.ID) + } + } + if mavenConfig != nil { + mavenProfiles = mavenConfig.MavenProfiles + } + childModules := []artifacts.ChildModule{} + if mavenConfig != nil { + childModules = mavenConfig.ChildModules + } else { + if pom.Modules != nil { + for _, module := range *pom.Modules { + relChildPomPath := module + if filepath.Ext(relChildPomPath) != ".xml" { + relChildPomPath = filepath.Join(relChildPomPath, maven.PomXMLFileName) + } + childPom := maven.Pom{} + childPomPath := filepath.Join(pomFileDir, relChildPomPath) + if err := childPom.Load(childPomPath); err != nil { + logrus.Errorf("failed to load the child pom.xml file at path %s . Error: %q", childPomPath, err) + continue + } + childModule := artifacts.ChildModule{Name: pom.ArtifactID, RelPomPath: relChildPomPath} + childModules = append(childModules, childModule) + } + } + } + deploymentFilePath, err := getDeploymentFilePathFromPom(pom, pomFileDir) + if err != nil { + logrus.Errorf("failed to get the deployment (jar/war/ear) file path for the pom.xml %+v . Error: %q", pom, err) + } + return infoT{ + Name: name, + Type: artifactType, + IsParentPom: isParentPom, + IsMvnwPresent: isMvnwPresent, + JavaVersion: javaVersion, + DeploymentFilePath: deploymentFilePath, + MavenProfiles: mavenProfiles, + ChildModules: childModules, + SpringBoot: getSpringBootConfigFromPom(pomFileDir, pom, parentPom), + }, nil +} + +func (t *MavenAnalyser) getDockerfileTemplate() (string, error) { + // multi stage build similar to https://nieldw.medium.com/caching-maven-dependencies-in-a-docker-build-dca6ca7ad612 + licenseFilePath := filepath.Join(t.Env.GetEnvironmentContext(), t.Env.RelTemplatesDir, "Dockerfile.license") + license, err := os.ReadFile(licenseFilePath) + if err != nil { + return "", fmt.Errorf("failed to read the Dockerfile license file at path %s . Error: %q", licenseFilePath, err) + } + mavenBuildTemplatePath := filepath.Join(t.Env.GetEnvironmentContext(), t.Env.RelTemplatesDir, "Dockerfile.maven-build") + mavenBuildTemplate, err := os.ReadFile(mavenBuildTemplatePath) + if err != nil { + return string(license), fmt.Errorf("failed to read the Dockerfile Maven build template file at path %s . Error: %q", mavenBuildTemplatePath, err) + } + return string(license) + "\n" + string(mavenBuildTemplate), nil +} + +func (t *MavenAnalyser) getJavaPackage(javaVersion string) (string, error) { + javaVersionToPackageMappingFilePath := filepath.Join(t.Env.GetEnvironmentContext(), versionMappingFilePath) + return getJavaPackage(javaVersionToPackageMappingFilePath, javaVersion) +} + +// helper functions + +func getDeploymentFilePathFromPom(pom *maven.Pom, pomFileDir string) (string, error) { + packaging := pom.Packaging + if packaging == "" { + packaging = string(artifacts.JarPackaging) + } + if pom.Build != nil { + if pom.Build.FinalName != "" { + return filepath.Join(pomFileDir, MAVEN_DEFAULT_BUILD_DIR, pom.Build.FinalName+"."+packaging), nil + } + if pom.Build.Plugins != nil { + for _, plugin := range *pom.Build.Plugins { + if plugin.ArtifactID != MAVEN_COMPILER_PLUGIN { + continue + } + if plugin.Configuration.FinalName != "" { + return filepath.Join(pomFileDir, MAVEN_DEFAULT_BUILD_DIR, plugin.Configuration.FinalName+"."+packaging), nil + } + } + } + } + return filepath.Join(pomFileDir, MAVEN_DEFAULT_BUILD_DIR, pom.ArtifactID+"-"+pom.Version+"."+packaging), nil +} + +func isParentPom(pom *maven.Pom) bool { + return pom.Packaging == string(artifacts.PomPackaging) || (pom.Modules != nil && len(*pom.Modules) > 0) +} + +func getJavaVersionFromPom(pom *maven.Pom) string { + if pom == nil { + return "" + } + if pom.Properties != nil && pom.Properties.Entries != nil { + jv, ok := pom.Properties.Entries["java.version"] + if ok && jv != "" { + return jv + } + jv, ok = pom.Properties.Entries["maven.compiler.target"] + if ok && jv != "" { + return jv + } + jv, ok = pom.Properties.Entries["maven.compiler.source"] + if ok && jv != "" { + return jv + } + } + if pom.Build != nil && pom.Build.Plugins != nil { + for _, plugin := range *pom.Build.Plugins { + if plugin.ArtifactID == MAVEN_COMPILER_PLUGIN { + if plugin.Configuration.Target != "" { + return plugin.Configuration.Target + } + if plugin.Configuration.Source != "" { + return plugin.Configuration.Source + } + } + } + } + return "" +} + +func packagingToArtifactType(packaging artifacts.JavaPackaging) (transformertypes.ArtifactType, error) { + switch strings.ToLower(string(packaging)) { + case string(artifacts.JarPackaging): + return artifacts.JarArtifactType, nil + case string(artifacts.WarPackaging): + return artifacts.WarArtifactType, nil + case string(artifacts.EarPackaging): + return artifacts.EarArtifactType, nil + default: + return transformertypes.ArtifactType(packaging), fmt.Errorf("the packaging type '%s' does not have a corresponding artifact type", packaging) + } +} + +func getSpringBootConfigFromPom(pomFileDir string, pom *maven.Pom, parentPom *maven.Pom) *artifacts.SpringBootConfig { + getSpringBootConfig := func(dependency maven.Dependency) *artifacts.SpringBootConfig { + if dependency.GroupID != springBootGroup { + return nil + } + springAppName, springProfiles, profilePorts := getSpringBootAppNameProfilesAndPortsFromDir(pomFileDir) + springConfig := &artifacts.SpringBootConfig{ + SpringBootVersion: dependency.Version, + SpringBootAppName: springAppName, + SpringBootProfilePorts: profilePorts, + } + if len(springProfiles) != 0 { + springConfig.SpringBootProfiles = &springProfiles + } + return springConfig + } + // look for spring boot + if pom.Dependencies != nil { + for _, dependency := range *pom.Dependencies { + if springConfig := getSpringBootConfig(dependency); springConfig != nil { + return springConfig + } + } + } + if pom.DependencyManagement != nil && pom.DependencyManagement.Dependencies != nil { + for _, dependency := range *pom.DependencyManagement.Dependencies { + if springConfig := getSpringBootConfig(dependency); springConfig != nil { + return springConfig + } + } + } + // look for spring boot in parent pom.xml + if parentPom != nil { + if parentPom.Dependencies != nil { + for _, dependency := range *parentPom.Dependencies { + if springConfig := getSpringBootConfig(dependency); springConfig != nil { + return springConfig + } + } + } + if parentPom.DependencyManagement != nil && parentPom.DependencyManagement.Dependencies != nil { + for _, dependency := range *parentPom.DependencyManagement.Dependencies { + if springConfig := getSpringBootConfig(dependency); springConfig != nil { + return springConfig + } + } + } + } + return nil +} diff --git a/transformer/dockerfilegenerator/java/springbootutils.go b/transformer/dockerfilegenerator/java/springbootutils.go new file mode 100644 index 0000000..1f3163c --- /dev/null +++ b/transformer/dockerfilegenerator/java/springbootutils.go @@ -0,0 +1,420 @@ +/* +* Copyright IBM Corporation 2021 +* +* 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 java + +// +//import ( +// "bufio" +// "bytes" +// "encoding/json" +// "fmt" +// "io" +// "os" +// "path/filepath" +// "strings" +// +// "github.com/konveyor/move2kube-wasm/common" +// //irtypes "github.com/konveyor/move2kube-wasm/types/ir" +// "github.com/magiconair/properties" +// "github.com/mikefarah/yq/v4/pkg/yqlib" +// "github.com/sirupsen/logrus" +// "github.com/spf13/cast" +// "gopkg.in/yaml.v3" +// //"k8s.io/kubernetes/pkg/apis/core" +//) +// +//const ( +// springBootAppNameKey = "spring.application.name" +// springBootServerPortKey = "server.port" +// springBootGroup = "org.springframework.boot" +// // If no profile is active, a default profile is enabled. +// // The name of the default profile is default and it can be tuned using the spring.profiles.default Environment property, +// // as shown in the following example: spring.profiles.default=none +// defaultSpringProfile = "default" // https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.profiles +// springBootSpringProfilesActiveKey = "spring.profiles.active" // https://docs.spring.io/spring-boot/docs/current/reference/html/application-properties.html#application-properties.core.spring.profiles.active +// springBootSpringProfilesKey = "spring.profiles" // Probably an alias for "spring.profiles.active"? Can't find it in the documentation +//) +// +//var ( +// defaultSpringBootResourcesPath = filepath.Join("src", "main", "resources") +//) +// +//// SpringBootMetadataFiles defines the lists of configuration files from spring boot applications +//type SpringBootMetadataFiles struct { +// bootstrapFiles []string +// bootstrapYamlFiles []string +// appPropFiles []string +// appYamlFiles []string +//} +// +//// FlattenedProperty defines the key value pair of the spring boot properties +//type FlattenedProperty struct { +// Name string +// Value string +//} +// +//func injectProperties(ir irtypes.IR, serviceName string) irtypes.IR { +// service, ok := ir.Services[serviceName] +// if !ok { +// return ir +// } +// const vCapMountDir = "/vcap" +// const vCapPropertyFile = "vcap-properties.yaml" +// const vCapVolumeName = "vcapsecretvolume" +// const propertyImportEnvKey = "SPRING_CONFIG_IMPORT" +// // Flatten the VCAP_* environment JSON values to create spring-boot properties +// vCapEnvList := []FlattenedProperty{} +// for _, storage := range ir.Storages { +// if storage.StorageType != irtypes.SecretKind { +// continue +// } +// if serviceName+common.VcapCfSecretSuffix != storage.Name { +// continue +// } +// for key, value := range storage.Content { +// env := core.EnvVar{Name: key, Value: string(value)} +// if key == common.VcapServiceEnvName { +// vCapEnvList = append(vCapEnvList, flattenToVcapServicesProperties(env)...) +// } else if key == common.VcapApplicationEnvName { +// vCapEnvList = append(vCapEnvList, flattenToVcapApplicationProperties(env)...) +// } +// } +// } +// if len(vCapEnvList) == 0 { +// return ir +// } +// // Dump the entire VCAP_* property key-value pair data as one large chunk of string data +// // which will then be used as value to the VCAP property file name. +// data := []string{} +// for _, vcapEnv := range vCapEnvList { +// data = append(data, vcapEnv.Name+": "+vcapEnv.Value) +// } +// // Create a secret for VCAP_* property key-value pairs +// secretName := serviceName + common.VcapSpringBootSecretSuffix +// ir.Storages = append(ir.Storages, irtypes.Storage{ +// Name: secretName, +// StorageType: irtypes.SecretKind, +// Content: map[string][]byte{ +// vCapPropertyFile: []byte(strings.Join(data, "\n")), +// }, +// }) +// // Create volume mount path for by assigning a pre-defined directory and property file. +// mountPath := filepath.Join(vCapMountDir, vCapPropertyFile) +// for i, container := range service.Containers { +// // Add an environment variable for SPRING_CONFIG_IMPORT and its value in every container +// container.Env = append(container.Env, core.EnvVar{Name: propertyImportEnvKey, Value: mountPath}) +// // Create volume mounts for each container of the service +// container.VolumeMounts = append(container.VolumeMounts, core.VolumeMount{Name: vCapVolumeName, MountPath: vCapMountDir}) +// service.Containers[i] = container +// } +// // Create a volume for each service which maps to the secret created for VCAP_* property key-value pairs +// service.Volumes = append(service.Volumes, +// core.Volume{ +// Name: vCapVolumeName, +// VolumeSource: core.VolumeSource{Secret: &core.SecretVolumeSource{SecretName: secretName}}, +// }) +// ir.Services[serviceName] = service +// return ir +//} +// +//// interfaceSliceToDelimitedString converts an interface slice to string slice +//func interfaceSliceToDelimitedString(intSlice []interface{}) string { +// var stringSlice []string +// for _, value := range intSlice { +// stringSlice = append(stringSlice, fmt.Sprintf("%v", value)) +// } +// return strings.Join(stringSlice, ",") +//} +// +//// flattenPropertyKey flattens a given variable defined by +//func flattenPropertyKey(prefix string, unflattenedValueI interface{}) []FlattenedProperty { +// var flattenedList []FlattenedProperty +// switch unflattenedValue := unflattenedValueI.(type) { +// case []interface{}: +// flattenedList = append(flattenedList, FlattenedProperty{Name: prefix, Value: interfaceSliceToDelimitedString(unflattenedValue)}) +// for i, value := range unflattenedValue { +// newPrefix := fmt.Sprintf("%s[%d]", prefix, i) +// flattenedList = append(flattenedList, flattenPropertyKey(newPrefix, value)...) +// } +// case map[string]interface{}: +// for key, value := range unflattenedValue { +// newPrefix := fmt.Sprintf("%s.%s", prefix, key) +// flattenedList = append(flattenedList, flattenPropertyKey(newPrefix, value)...) +// } +// case string: +// return []FlattenedProperty{{Name: prefix, Value: unflattenedValue}} +// case int: +// return []FlattenedProperty{{Name: prefix, Value: cast.ToString(unflattenedValue)}} +// case bool: +// return []FlattenedProperty{{Name: prefix, Value: fmt.Sprintf("%t", unflattenedValue)}} +// default: +// if unflattenedValue != nil { +// return []FlattenedProperty{{Name: prefix, Value: fmt.Sprintf("%#v", unflattenedValue)}} +// } +// return []FlattenedProperty{{Name: prefix, Value: ""}} +// } +// return flattenedList +//} +// +//// flattenToVcapServicesProperties flattens the variables specified in VCAP_SERVICES +//func flattenToVcapServicesProperties(env core.EnvVar) []FlattenedProperty { +// serviceInstanceIsMap := map[string][]interface{}{} +// if err := json.Unmarshal([]byte(env.Value), &serviceInstanceIsMap); err != nil { +// logrus.Errorf("failed to unmarshal as JSON the value of the environment variable: %+v . Error: %q", env, err) +// return nil +// } +// flattenedEnvList := []FlattenedProperty{} +// for _, serviceInstanceIs := range serviceInstanceIsMap { +// for _, serviceInstanceI := range serviceInstanceIs { +// serviceInstance, ok := serviceInstanceI.(map[string]interface{}) +// if !ok { +// continue +// } +// key := "" +// if serviceName, ok := serviceInstance["name"].(string); ok { +// key = serviceName +// } else if labelName, ok := serviceInstance["label"].(string); ok { +// key = labelName +// } +// flattenedEnvList = append(flattenedEnvList, flattenPropertyKey("vcap.services."+key, serviceInstance)...) +// } +// } +// return flattenedEnvList +//} +// +//// flattenToVcapApplicationProperties flattens the variables specified in VCAP_APPLICATION +//func flattenToVcapApplicationProperties(env core.EnvVar) []FlattenedProperty { +// var serviceInstanceMap map[string]interface{} +// err := json.Unmarshal([]byte(env.Value), &serviceInstanceMap) +// if err != nil { +// logrus.Errorf("Could not unmarshal the service map instance (%s): %s", env.Name, err) +// return nil +// } +// return flattenPropertyKey("vcap.application", serviceInstanceMap) +//} +// +//func getSpringBootAppNameProfilesAndPortsFromDir(dir string) (string, []string, map[string][]int32) { +// return getSpringBootAppNameProfilesAndPorts(getSpringBootMetadataFiles(dir)) +//} +// +//func getSpringBootMetadataFiles(dir string) SpringBootMetadataFiles { +// springbootMetadataFiles := SpringBootMetadataFiles{} +// resourcesPath := filepath.Join(dir, defaultSpringBootResourcesPath) +// var err error +// springbootMetadataFiles.bootstrapFiles, err = common.GetFilesByName(resourcesPath, []string{"bootstrap.properties"}, nil) +// if err != nil { +// logrus.Debugf("failed to get files by name for path %s for bootstrap.properties. Error: %q", resourcesPath, err) +// } +// springbootMetadataFiles.bootstrapYamlFiles, err = common.GetFilesByName(resourcesPath, nil, []string{`bootstrap\.ya?ml`}) +// if err != nil { +// logrus.Debugf("failed to get files by name for path %s bootstrap.yaml. Error: %q", resourcesPath, err) +// } +// springbootMetadataFiles.appPropFiles, err = common.GetFilesByName(resourcesPath, nil, []string{`application(-.+)?\.properties`}) +// if err != nil { +// logrus.Debugf("failed to get files by name for path %s application.properties. Error: %q", resourcesPath, err) +// } +// springbootMetadataFiles.appYamlFiles, err = common.GetFilesByName(resourcesPath, nil, []string{`application(-.+)?\.ya?ml`}) +// if err != nil { +// logrus.Debugf("failed to get files by name for path %s application.yaml. Error: %q", resourcesPath, err) +// } +// return springbootMetadataFiles +//} +// +//func getSpringBootAppNameProfilesAndPorts(springbootMetadataFiles SpringBootMetadataFiles) (appName string, profiles []string, profilePorts map[string][]int32) { +// appName = "" +// profiles = []string{} +// profilePorts = map[string][]int32{} +// // find sping boot app name from bootstrap.properties or bootstrap.yaml +// if len(springbootMetadataFiles.bootstrapFiles) != 0 { +// props, err := properties.LoadFiles(springbootMetadataFiles.bootstrapFiles, properties.UTF8, false) +// if err != nil { +// logrus.Errorf("failed to load the bootstrap properties files from paths %+v . Error: %q", springbootMetadataFiles.bootstrapFiles, err) +// } else { +// appName = props.GetString(springBootAppNameKey, "") +// } +// } else if len(springbootMetadataFiles.bootstrapYamlFiles) != 0 { +// propss := convertYamlDocumentsToProperties(getYamlDocumentsFromFiles(springbootMetadataFiles.bootstrapYamlFiles)) +// for _, props := range propss { +// if appName = props.GetString(springBootAppNameKey, ""); appName != "" { +// break +// } +// } +// } +// // find spring boot app name from application.properties +// for _, appPropFilePath := range springbootMetadataFiles.appPropFiles { +// // TODO: handle multi-document properties files https://spring.io/blog/2020/08/14/config-file-processing-in-spring-boot-2-4#multi-document-properties-files +// props, err := properties.LoadFile(appPropFilePath, properties.UTF8) +// if err != nil { +// logrus.Errorf("failed to load the file at path %s as a properties file. Error: %q", appPropFilePath, err) +// continue +// } +// appPropFilename := filepath.Base(appPropFilePath) +// if appPropFilename == "application.properties" { +// // get app name +// appName = props.GetString(springBootAppNameKey, appName) +// // get active profiles +// // https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.profiles +// activeProfilesStr := props.GetString(springBootSpringProfilesActiveKey, "") +// if activeProfilesStr == "" { +// activeProfilesStr = props.GetString(springBootSpringProfilesKey, "") +// } +// activeProfiles := getSpringProfiles(activeProfilesStr) +// // add to list of known spring profiles +// profiles = common.AppendIfNotPresent(profiles, activeProfiles...) +// // get ports +// if appPort := props.GetInt(springBootServerPortKey, -1); appPort != -1 { +// if len(activeProfiles) > 0 { +// for _, activeProfile := range activeProfiles { +// profilePorts[activeProfile] = append(profilePorts[activeProfile], int32(appPort)) +// } +// } else { +// profilePorts[defaultSpringProfile] = append(profilePorts[defaultSpringProfile], int32(appPort)) +// } +// } +// } else { +// activeProfile := strings.TrimSuffix(strings.TrimPrefix(appPropFilename, "application-"), ".properties") +// if activeProfile == "" { +// logrus.Warnf("invalid/empty spring profile name for the properties file at path %s", appPropFilePath) +// continue +// } +// // add to list of known spring profiles +// profiles = common.AppendIfNotPresent(profiles, activeProfile) +// // get ports +// if appPort := props.GetInt(springBootServerPortKey, -1); appPort != -1 { +// profilePorts[activeProfile] = append(profilePorts[activeProfile], int32(appPort)) +// } +// // TODO: should we try to get app name for each profile as well? +// } +// } +// // find spring boot app name from application.yaml +// for _, appYamlFilePath := range springbootMetadataFiles.appYamlFiles { +// // TODO: handle multi document yamls +// propss := convertYamlDocumentsToProperties(getYamlDocumentsFromFiles([]string{appYamlFilePath})) +// if len(propss) == 0 { +// logrus.Warnf("parsed out an empty properties struct from the file at path %s", appYamlFilePath) +// continue +// } +// props := propss[0] +// for _, p := range propss[1:] { +// props.Merge(p) +// } +// // get app name +// appName = props.GetString(springBootAppNameKey, appName) +// // get ports and profiles +// appYamlFilename := filepath.Base(appYamlFilePath) +// if appYamlFilename == "application.yml" || appYamlFilename == "application.yaml" { +// activeProfilesStr := props.GetString(springBootSpringProfilesActiveKey, "") +// if activeProfilesStr == "" { +// activeProfilesStr = props.GetString(springBootSpringProfilesKey, "") +// } +// activeProfiles := getSpringProfiles(activeProfilesStr) +// // add to list of known spring profiles +// profiles = common.AppendIfNotPresent(profiles, activeProfiles...) +// // get ports +// if appPort := props.GetInt(springBootServerPortKey, -1); appPort != -1 { +// if len(activeProfiles) > 0 { +// for _, activeProfile := range activeProfiles { +// profilePorts[activeProfile] = append(profilePorts[activeProfile], int32(appPort)) +// } +// } else { +// profilePorts[defaultSpringProfile] = append(profilePorts[defaultSpringProfile], int32(appPort)) +// } +// } +// } else { +// activeProfile := strings.TrimSuffix(strings.TrimPrefix(appYamlFilename, "application-"), filepath.Ext(appYamlFilename)) +// if activeProfile == "" { +// logrus.Warnf("invalid/empty spring profile name for the properties file at path %s", appYamlFilePath) +// continue +// } +// // add to list of known spring profiles +// profiles = common.AppendIfNotPresent(profiles, activeProfile) +// // get ports +// if appPort := props.GetInt(springBootServerPortKey, -1); appPort != -1 { +// profilePorts[activeProfile] = append(profilePorts[activeProfile], int32(appPort)) +// } +// // TODO: should we try to get app name for each profile as well? +// } +// } +// return appName, profiles, profilePorts +//} +// +//func getYamlDocumentsFromFile(filePath string) ([][]byte, error) { +// fileBytes, err := os.ReadFile(filePath) +// if err != nil { +// logrus.Errorf("failed to read file at path %s . Error: %q", filePath, err) +// return nil, nil +// } +// return common.SplitYAML(fileBytes) +//} +// +//func getYamlDocumentsFromFiles(filePaths []string) []string { +// segments := []string{} +// for _, filePath := range filePaths { +// docs, err := getYamlDocumentsFromFile(filePath) +// if err != nil { +// logrus.Errorf("failed to get YAML documents for the file at path %s , skipping. Error: %q", filePath, err) +// continue +// } +// for _, doc := range docs { +// segments = append(segments, string(doc)) +// } +// } +// return segments +//} +// +//func getSpringProfiles(springProfilesStr string) []string { +// rawSpringProfiles := strings.Split(springProfilesStr, ",") +// springProfiles := []string{} +// for _, rawSpringProfile := range rawSpringProfiles { +// springProfile := strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(rawSpringProfile), "!")) +// if springProfile != "" { +// springProfiles = append(springProfiles, rawSpringProfile) +// } +// } +// return springProfiles +//} +// +//func convertYamlDocumentToProperties(doc string) (props *properties.Properties, err error) { +// decoder := yaml.NewDecoder(strings.NewReader(doc)) +// var dataBucket yaml.Node +// errorReading := decoder.Decode(&dataBucket) +// if errorReading != io.EOF && errorReading != nil { +// return nil, errorReading +// } +// var output bytes.Buffer +// writer := bufio.NewWriter(&output) +// propsEncoder := yqlib.NewPropertiesEncoder(writer) +// err = propsEncoder.Encode(&dataBucket) +// if err != nil { +// logrus.Errorf("Error while encoding to properties : %s", err) +// return nil, err +// } +// writer.Flush() +// return properties.LoadString(output.String()) +//} +// +//func convertYamlDocumentsToProperties(docs []string) []*properties.Properties { +// props := []*properties.Properties{} +// for _, doc := range docs { +// prop, err := convertYamlDocumentToProperties(doc) +// if err != nil { +// logrus.Errorf("failed to decode the YAML document as properties. Document: %s . Error: %q", doc, err) +// } +// props = append(props, prop) +// } +// return props +//} diff --git a/transformer/dockerfilegenerator/java/tomcat.go b/transformer/dockerfilegenerator/java/tomcat.go new file mode 100644 index 0000000..3fdf34f --- /dev/null +++ b/transformer/dockerfilegenerator/java/tomcat.go @@ -0,0 +1,215 @@ +/* + * Copyright IBM Corporation 2021 + * + * 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 java + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/konveyor/move2kube-wasm/common" + "github.com/konveyor/move2kube-wasm/environment" + //irtypes "github.com/konveyor/move2kube-wasm/types/ir" + transformertypes "github.com/konveyor/move2kube-wasm/types/transformer" + "github.com/konveyor/move2kube-wasm/types/transformer/artifacts" + "github.com/sirupsen/logrus" +) + +const ( + tomcatDefaultPort = 8080 +) + +// Tomcat implements Transformer interface +type Tomcat struct { + Config transformertypes.Transformer + Env *environment.Environment + TomcatConfig *TomcatYamlConfig +} + +// TomcatYamlConfig stores jar related configuration information +type TomcatYamlConfig struct { + JavaVersion string `yaml:"defaultJavaVersion"` +} + +// TomcatDockerfileTemplate stores parameters for the dockerfile template +type TomcatDockerfileTemplate struct { + JavaPackageName string + JavaVersion string + DeploymentFilePath string + BuildContainerName string + Port int32 + EnvVariables map[string]string +} + +// Init Initializes the transformer +func (t *Tomcat) Init(tc transformertypes.Transformer, env *environment.Environment) (err error) { + t.Config = tc + t.Env = env + t.TomcatConfig = &TomcatYamlConfig{} + err = common.GetObjFromInterface(t.Config.Spec.Config, t.TomcatConfig) + if err != nil { + logrus.Errorf("unable to load config for Transformer %+v into %T : %s", t.Config.Spec.Config, t.TomcatConfig, err) + return err + } + if t.TomcatConfig.JavaVersion == "" { + t.TomcatConfig.JavaVersion = defaultJavaVersion + } + return nil +} + +// GetConfig returns the transformer config +func (t *Tomcat) GetConfig() (transformertypes.Transformer, *environment.Environment) { + return t.Config, t.Env +} + +// DirectoryDetect runs detect in each sub directory +func (t *Tomcat) DirectoryDetect(dir string) (services map[string][]transformertypes.Artifact, err error) { + return +} + +// Transform transforms the artifacts +func (t *Tomcat) Transform(newArtifacts []transformertypes.Artifact, alreadySeenArtifacts []transformertypes.Artifact) ([]transformertypes.PathMapping, []transformertypes.Artifact, error) { + pathMappings := []transformertypes.PathMapping{} + createdArtifacts := []transformertypes.Artifact{} + for _, newArtifact := range newArtifacts { + if newArtifact.Type != artifacts.WarArtifactType { + continue + } + serviceConfig := artifacts.ServiceConfig{} + if err := newArtifact.GetConfig(artifacts.ServiceConfigType, &serviceConfig); err != nil { + logrus.Debugf("failed to load service config from the artifact: %+v . Error: %q", newArtifact, err) + continue + } + if serviceConfig.ServiceName == "" { + serviceConfig.ServiceName = common.MakeStringK8sServiceNameCompliant(newArtifact.Name) + } + imageName := artifacts.ImageName{} + if err := newArtifact.GetConfig(artifacts.ImageNameConfigType, &imageName); err != nil { + logrus.Debugf("failed to load image name config from the artifact: %+v . Error: %q", imageName, err) + } + if imageName.ImageName == "" { + imageName.ImageName = common.MakeStringContainerImageNameCompliant(newArtifact.Name) + } + if len(newArtifact.Paths[artifacts.ServiceDirPathType]) == 0 { + logrus.Errorf("service directory missing from artifact: %+v", newArtifact) + continue + } + serviceDir := newArtifact.Paths[artifacts.ServiceDirPathType][0] + relServiceDir, err := filepath.Rel(t.Env.GetEnvironmentSource(), serviceDir) + if err != nil { + logrus.Errorf("failed to make the source path %s relative to the source code directory %s . Error: %q", serviceDir, t.Env.GetEnvironmentSource(), err) + continue + } + dockerfileTemplate, err := t.getDockerfileTemplate(newArtifact) + if err != nil { + logrus.Errorf("failed to get the tomcat Dockerfile template. Error: %q", err) + continue + } + tempDir := filepath.Join(t.Env.TempPath, newArtifact.Name) + if err := os.MkdirAll(tempDir, common.DefaultDirectoryPermission); err != nil { + logrus.Errorf("failed to create the temporary directory %s . Error: %q", tempDir, err) + continue + } + dockerfileTemplatePath := filepath.Join(tempDir, common.DefaultDockerfileName) + if err := os.WriteFile(dockerfileTemplatePath, []byte(dockerfileTemplate), common.DefaultFilePermission); err != nil { + logrus.Errorf("failed to write the tomcat Dockerfile template to a temporary file at path %s . Error: %q", dockerfileTemplatePath, err) + continue + } + warConfig := artifacts.WarArtifactConfig{} + if err := newArtifact.GetConfig(artifacts.WarConfigType, &warConfig); err != nil { + logrus.Errorf("failed to load the war config from the artifact %+v . Error: %q", newArtifact, err) + continue + } + if warConfig.JavaVersion == "" { + warConfig.JavaVersion = t.TomcatConfig.JavaVersion + } + javaPackage, err := getJavaPackage(filepath.Join(t.Env.GetEnvironmentContext(), versionMappingFilePath), warConfig.JavaVersion) + if err != nil { + logrus.Errorf("failed to find mapping version for java version %s . Error: %q", warConfig.JavaVersion, err) + javaPackage = defaultJavaPackage + } + pathMappings = append(pathMappings, transformertypes.PathMapping{ + Type: transformertypes.SourcePathMappingType, + DestPath: common.DefaultSourceDir, + }) + templateData := TomcatDockerfileTemplate{ + JavaPackageName: javaPackage, + JavaVersion: warConfig.JavaVersion, + DeploymentFilePath: warConfig.DeploymentFilePath, + Port: tomcatDefaultPort, + EnvVariables: warConfig.EnvVariables, + BuildContainerName: warConfig.BuildContainerName, + } + pathMappings = append(pathMappings, transformertypes.PathMapping{ + Type: transformertypes.TemplatePathMappingType, + SrcPath: dockerfileTemplatePath, + DestPath: filepath.Join(common.DefaultSourceDir, relServiceDir), + TemplateConfig: templateData, + }) + paths := newArtifact.Paths + paths[artifacts.DockerfilePathType] = []string{filepath.Join(common.DefaultSourceDir, relServiceDir, common.DefaultDockerfileName)} + dockerfileArtifact := transformertypes.Artifact{ + Name: imageName.ImageName, + Type: artifacts.DockerfileArtifactType, + Paths: paths, + Configs: map[transformertypes.ConfigType]interface{}{ + artifacts.ImageNameConfigType: imageName, + }, + } + dockerfileServiceArtifact := transformertypes.Artifact{ + Name: serviceConfig.ServiceName, + Type: artifacts.DockerfileForServiceArtifactType, + Paths: newArtifact.Paths, + Configs: map[transformertypes.ConfigType]interface{}{ + artifacts.ImageNameConfigType: imageName, + artifacts.ServiceConfigType: serviceConfig, + }, + } + //TODO: WASI + //ir := irtypes.IR{} + //if err = newArtifact.GetConfig(irtypes.IRConfigType, &ir); err == nil { + // dockerfileServiceArtifact.Configs[irtypes.IRConfigType] = ir + //} + createdArtifacts = append(createdArtifacts, dockerfileArtifact, dockerfileServiceArtifact) + } + return pathMappings, createdArtifacts, nil +} + +func (t *Tomcat) getDockerfileTemplate(newArtifact transformertypes.Artifact) (string, error) { + tomcatRunTemplatePath := filepath.Join(t.Env.GetEnvironmentContext(), t.Env.RelTemplatesDir, "Dockerfile.tomcat") + tomcatRunTemplate, err := os.ReadFile(tomcatRunTemplatePath) + if err != nil { + return "", fmt.Errorf("failed to read the tomcat run stage Dockerfile template at path %s . Error: %q", tomcatRunTemplatePath, err) + } + dockerFileHead := "" + if buildContainerPaths := newArtifact.Paths[artifacts.BuildContainerFileType]; len(buildContainerPaths) > 0 { + dockerfileBuildPath := buildContainerPaths[0] + dockerFileHeadBytes, err := os.ReadFile(dockerfileBuildPath) + if err != nil { + return "", fmt.Errorf("failed to read the build stage Dockerfile template at path %s . Error: %q", dockerfileBuildPath, err) + } + dockerFileHead = string(dockerFileHeadBytes) + } else { + licenseFilePath := filepath.Join(t.Env.GetEnvironmentContext(), t.Env.RelTemplatesDir, "Dockerfile.license") + dockerFileHeadBytes, err := os.ReadFile(licenseFilePath) + if err != nil { + return "", fmt.Errorf("failed to read the Dockerfile license at path %s . Error: %q", licenseFilePath, err) + } + dockerFileHead = string(dockerFileHeadBytes) + } + return dockerFileHead + "\n" + string(tomcatRunTemplate), nil +} diff --git a/transformer/dockerfilegenerator/java/types.go b/transformer/dockerfilegenerator/java/types.go new file mode 100644 index 0000000..c230fcb --- /dev/null +++ b/transformer/dockerfilegenerator/java/types.go @@ -0,0 +1,39 @@ +/* + * Copyright IBM Corporation 2021 + * + * 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 java + +import ( + "github.com/konveyor/move2kube-wasm/types" +) + +const ( + versionMappingFilePath = "mappings/javapackageversions.yaml" + // JavaPackageNamesMappingKind defines kind of JavaPackageNamesMappingKind + JavaPackageNamesMappingKind types.Kind = "JavaPackageVersions" +) + +// JavaPackageNamesMapping stores the java package version mappings +type JavaPackageNamesMapping struct { + types.TypeMeta `yaml:",inline"` + types.ObjectMeta `yaml:"metadata,omitempty"` + Spec JavaPackageNamesMappingSpec `yaml:"spec,omitempty"` +} + +// JavaPackageNamesMappingSpec stores the java package version spec +type JavaPackageNamesMappingSpec struct { + PackageVersions map[string]string `yaml:"packageVersions"` +} diff --git a/transformer/dockerfilegenerator/java/utils.go b/transformer/dockerfilegenerator/java/utils.go new file mode 100644 index 0000000..ed21b0b --- /dev/null +++ b/transformer/dockerfilegenerator/java/utils.go @@ -0,0 +1,81 @@ +/* + * Copyright IBM Corporation 2021 + * + * 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 java + +import ( + "fmt" + + "github.com/konveyor/move2kube-wasm/common" + //"github.com/konveyor/move2kube-wasm/qaengine" + "github.com/sirupsen/logrus" +) + +type buildOption string + +const ( + // NO_BUILD_STAGE don't generate the build stage in Dockerfiles + NO_BUILD_STAGE buildOption = "no build stage" + // BUILD_IN_BASE_IMAGE generate the build stage and put it in a separate Dockerfile + BUILD_IN_BASE_IMAGE buildOption = "build stage in base image" + // BUILD_IN_EVERY_IMAGE generate the build stage in every Dockerfile + BUILD_IN_EVERY_IMAGE buildOption = "build stage in every image" +) + +const ( + defaultAppPathInContainer = "/app" + defaultJavaVersion = "17" + defaultJavaPackage = "java-17-openjdk-devel" +) + +func getJavaPackage(mappingFile string, version string) (pkg string, err error) { + var javaPackageNamesMapping JavaPackageNamesMapping + if err := common.ReadMove2KubeYaml(mappingFile, &javaPackageNamesMapping); err != nil { + logrus.Debugf("Could not load mapping at %s", mappingFile) + return "", err + } + v, ok := javaPackageNamesMapping.Spec.PackageVersions[version] + if !ok { + logrus.Infof("Matching java package not found for java version : %s. Going with default.", version) + return defaultJavaPackage, nil + } + return v, nil +} + +// askUserForDockerfileType asks the user what type of Dockerfiles to generate. +func askUserForDockerfileType(rootProjectName string) (buildOption, error) { + //TODO: WASI + //quesId := common.JoinQASubKeys(common.ConfigServicesKey, `"`+rootProjectName+`"`, "dockerfileType") + //desc := fmt.Sprintf("What type of Dockerfiles should be generated for the service '%s'?", rootProjectName) + //options := []string{ + // string(NO_BUILD_STAGE), + // string(BUILD_IN_BASE_IMAGE), + // string(BUILD_IN_EVERY_IMAGE), + //} + def := BUILD_IN_BASE_IMAGE + //hints := []string{ + // fmt.Sprintf("[%s] There is no build stage. Dockerfiles will only contain the run stage. The jar/war/ear files will need to be built and present in the file system already, for them to get copied into the container.", NO_BUILD_STAGE), + // fmt.Sprintf("[%s] Put the build stage in a separate Dockerfile and create a base image.", BUILD_IN_BASE_IMAGE), + // fmt.Sprintf("[%s] Put the build stage in every Dockerfile to make it self contained. (Warning: This may cause one build per Dockerfile.)", BUILD_IN_EVERY_IMAGE), + //} + //selectedBuildOption := buildOption(qaengine.FetchSelectAnswer(quesId, desc, hints, string(def), options, nil)) + selectedBuildOption := NO_BUILD_STAGE + switch selectedBuildOption { + case NO_BUILD_STAGE, BUILD_IN_BASE_IMAGE, BUILD_IN_EVERY_IMAGE: + return selectedBuildOption, nil + } + return def, fmt.Errorf("user selected an unsupported option for generating Dockerfiles. Actual: %s", selectedBuildOption) +} diff --git a/transformer/dockerfilegenerator/java/waranalyser.go b/transformer/dockerfilegenerator/java/waranalyser.go new file mode 100644 index 0000000..c42945c --- /dev/null +++ b/transformer/dockerfilegenerator/java/waranalyser.go @@ -0,0 +1,113 @@ +/* + * Copyright IBM Corporation 2021 + * + * 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 java + +import ( + "fmt" + "path/filepath" + "strings" + + "github.com/konveyor/move2kube-wasm/common" + "github.com/konveyor/move2kube-wasm/environment" + transformertypes "github.com/konveyor/move2kube-wasm/types/transformer" + "github.com/konveyor/move2kube-wasm/types/transformer/artifacts" + "github.com/sirupsen/logrus" +) + +// WarAnalyser implements Transformer interface +type WarAnalyser struct { + Config transformertypes.Transformer + Env *environment.Environment + WarConfig *WarYamlConfig +} + +// WarYamlConfig stores the war related information +type WarYamlConfig struct { + JavaVersion string `yaml:"defaultJavaVersion"` +} + +// WarDockerfileTemplate stores parameters for the dockerfile template +type WarDockerfileTemplate struct { + DeploymentFile string + BuildContainerName string + DeploymentFileDirInBuildContainer string + EnvVariables map[string]string +} + +// Init Initializes the transformer +func (t *WarAnalyser) Init(tc transformertypes.Transformer, env *environment.Environment) (err error) { + t.Config = tc + t.Env = env + t.WarConfig = &WarYamlConfig{} + err = common.GetObjFromInterface(t.Config.Spec.Config, t.WarConfig) + if err != nil { + logrus.Errorf("unable to load config for Transformer %+v into %T : %s", t.Config.Spec.Config, t.WarConfig, err) + return err + } + if t.WarConfig.JavaVersion == "" { + t.WarConfig.JavaVersion = defaultJavaVersion + } + return nil +} + +// GetConfig returns the transformer config +func (t *WarAnalyser) GetConfig() (transformertypes.Transformer, *environment.Environment) { + return t.Config, t.Env +} + +// DirectoryDetect runs detect in each sub directory +func (t *WarAnalyser) DirectoryDetect(dir string) (map[string][]transformertypes.Artifact, error) { + services := map[string][]transformertypes.Artifact{} + paths, err := common.GetFilesInCurrentDirectory(dir, nil, []string{`\.war$`}) + if err != nil { + return services, fmt.Errorf("failed to look for .war archives in the directory %s . Error: %q", dir, err) + } + if len(paths) == 0 { + return nil, nil + } + for _, path := range paths { + serviceName := strings.TrimSuffix(filepath.Base(path), filepath.Ext(path)) + normalizedServiceName := common.MakeStringK8sServiceNameCompliant(serviceName) + imageName := common.MakeStringContainerImageNameCompliant(serviceName) + newArtifact := transformertypes.Artifact{ + Paths: map[transformertypes.PathType][]string{ + artifacts.WarPathType: {path}, + artifacts.ServiceDirPathType: {dir}, + }, + Configs: map[transformertypes.ConfigType]interface{}{ + artifacts.WarConfigType: artifacts.WarArtifactConfig{ + DeploymentFilePath: filepath.Base(path), + JavaVersion: t.WarConfig.JavaVersion, + }, + artifacts.ImageNameConfigType: artifacts.ImageName{ImageName: imageName}, + }, + } + services[normalizedServiceName] = append(services[normalizedServiceName], newArtifact) + } + return services, nil +} + +// Transform transforms the artifacts +func (t *WarAnalyser) Transform(newArtifacts []transformertypes.Artifact, alreadySeenArtifacts []transformertypes.Artifact) ([]transformertypes.PathMapping, []transformertypes.Artifact, error) { + pathMappings := []transformertypes.PathMapping{} + createdArtifacts := []transformertypes.Artifact{} + for _, a := range newArtifacts { + a.Type = artifacts.WarArtifactType + createdArtifacts = append(createdArtifacts, a) + } + return pathMappings, createdArtifacts, nil +} diff --git a/transformer/dockerfilegenerator/java/zuulanalyser.go b/transformer/dockerfilegenerator/java/zuulanalyser.go new file mode 100644 index 0000000..3952aa8 --- /dev/null +++ b/transformer/dockerfilegenerator/java/zuulanalyser.go @@ -0,0 +1,110 @@ +/* + * Copyright IBM Corporation 2021 + * + * 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 java + +import ( + "strings" + + "github.com/konveyor/move2kube-wasm/common" + "github.com/konveyor/move2kube-wasm/environment" + //irtypes "github.com/konveyor/move2kube-wasm/types/ir" + transformertypes "github.com/konveyor/move2kube-wasm/types/transformer" + "github.com/sirupsen/logrus" +) + +// ZuulAnalyser implements Transformer interface +type ZuulAnalyser struct { + Config transformertypes.Transformer + Env *environment.Environment + services map[string]string +} + +// ZuulSpec defines zuul specification +type ZuulSpec struct { + RouteSpec map[string]string `yaml:"routes"` +} + +// Zuul defines zuul spring boot properties file +type Zuul struct { + ZuulSpec ZuulSpec `yaml:"zuul"` +} + +// Init Initializes the transformer +func (t *ZuulAnalyser) Init(tc transformertypes.Transformer, env *environment.Environment) (err error) { + t.Config = tc + t.Env = env + envSource := env.GetEnvironmentSource() + var yamlpaths []string + if envSource != "" { + yamlpaths, err = common.GetFilesByExt(env.GetEnvironmentSource(), []string{".yaml", ".yml"}) + if err != nil { + logrus.Errorf("Unable to fetch yaml files at path %s Error: %q", env.GetEnvironmentSource(), err) + return err + } + } + t.services = map[string]string{} + for _, path := range yamlpaths { + z := Zuul{} + if err := common.ReadYaml(path, &z); err != nil || z.ZuulSpec.RouteSpec == nil { + continue + } + for servicename, routepath := range z.ZuulSpec.RouteSpec { + // TODO: routepath (ant style) to regex + routepath = strings.TrimSuffix(routepath, "**") + t.services[servicename] = routepath + } + } + return nil +} + +// GetConfig returns the transformer config +func (t *ZuulAnalyser) GetConfig() (transformertypes.Transformer, *environment.Environment) { + return t.Config, t.Env +} + +// DirectoryDetect runs detect in base directory +func (t *ZuulAnalyser) DirectoryDetect(dir string) (services map[string][]transformertypes.Artifact, err error) { + return nil, nil +} + +// Transform transforms the artifacts +func (t *ZuulAnalyser) Transform(newArtifacts []transformertypes.Artifact, alreadySeenArtifacts []transformertypes.Artifact) ([]transformertypes.PathMapping, []transformertypes.Artifact, error) { + //TODO: WASI + //artifactsCreated := []transformertypes.Artifact{} + //for _, a := range newArtifacts { + // ir := irtypes.IR{} + // if err := a.GetConfig(irtypes.IRConfigType, &ir); err != nil { + // logrus.Errorf("unable to load config for Transformer into %T Error: %q", ir, err) + // continue + // } + // for sn, s := range ir.Services { + // if r, ok := t.services[sn]; ok { + // if len(s.ServiceToPodPortForwardings) > 0 { + // s.ServiceToPodPortForwardings[0].ServiceRelPath = r + // } + // if s.Annotations == nil { + // s.Annotations = map[string]string{} + // } + // } + // ir.Services[sn] = s + // } + // a.Configs[irtypes.IRConfigType] = ir + // artifactsCreated = append(artifactsCreated, a) + //} + //return nil, artifactsCreated, nil + return nil, nil, nil +} diff --git a/transformer/dockerfilegenerator/nodejsdockerfiletransformer.go b/transformer/dockerfilegenerator/nodejsdockerfiletransformer.go new file mode 100644 index 0000000..5bc475d --- /dev/null +++ b/transformer/dockerfilegenerator/nodejsdockerfiletransformer.go @@ -0,0 +1,329 @@ +/* + * Copyright IBM Corporation 2021 + * + * 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 dockerfilegenerator + +import ( + "fmt" + //"os" + "path/filepath" + "sort" + "strings" + + //"github.com/joho/godotenv" + "github.com/konveyor/move2kube-wasm/common" + "github.com/konveyor/move2kube-wasm/environment" + "github.com/konveyor/move2kube-wasm/types" + //irtypes "github.com/konveyor/move2kube-wasm/types/ir" + //"github.com/konveyor/move2kube-wasm/types/qaengine/commonqa" + transformertypes "github.com/konveyor/move2kube-wasm/types/transformer" + "github.com/konveyor/move2kube-wasm/types/transformer/artifacts" + "github.com/sirupsen/logrus" + //"github.com/spf13/cast" + "golang.org/x/mod/semver" +) + +// ----------------------------------------------------------------------------------- +// Struct to load the package.json file +// ----------------------------------------------------------------------------------- + +// PackageJSON represents NodeJS package.json +type PackageJSON struct { + Private bool `json:"private"` + Name string `json:"name"` + Version string `json:"version"` + Description string `json:"description"` + Homepage string `json:"homepage"` + License string `json:"license"` + Main string `json:"main"` + PackageManager string `json:"packageManager,omitempty"` + Keywords []string `json:"keywords,omitempty"` + Files []string `json:"files,omitempty"` + Os []string `json:"os,omitempty"` + CPU []string `json:"cpu,omitempty"` + Scripts map[string]string `json:"scripts,omitempty"` + Engines map[string]string `json:"engines,omitempty"` + Dependencies map[string]string `json:"dependencies,omitempty"` +} + +// NodejsTemplateConfig implements Nodejs config interface +type NodejsTemplateConfig struct { + Port int32 + Build bool + NodeVersion string + NodeImageTag string + NodeMajorVersion string + NodeVersionProperties map[string]string + PackageManager string +} + +// ----------------------------------------------------------------------------------- +// Mappings file +// ----------------------------------------------------------------------------------- + +// NodeVersionsMapping stores the Node versions mapping +type NodeVersionsMapping struct { + types.TypeMeta `yaml:",inline"` + types.ObjectMeta `yaml:"metadata,omitempty"` + Spec NodeVersionsMappingSpec `yaml:"spec,omitempty"` +} + +// NodeVersionsMappingSpec stores the Node version spec +type NodeVersionsMappingSpec struct { + DisableSort bool `yaml:"disableSort"` + NodeVersions []map[string]string `yaml:"nodeVersions"` +} + +// ----------------------------------------------------------------------------------- +// Transformer +// ----------------------------------------------------------------------------------- + +// NodejsDockerfileGenerator implements the Transformer interface +type NodejsDockerfileGenerator struct { + Config transformertypes.Transformer + Env *environment.Environment + NodejsConfig *NodejsDockerfileYamlConfig + Spec NodeVersionsMappingSpec +} + +// NodejsDockerfileYamlConfig represents the configuration of the Nodejs dockerfile +type NodejsDockerfileYamlConfig struct { + DefaultNodejsVersion string `yaml:"defaultNodejsVersion"` + DefaultPackageManager string `yaml:"defaultPackageManager"` +} + +const ( + packageJSONFile = "package.json" + versionMappingFilePath = "mappings/nodeversions.yaml" + defaultPackageManager = "npm" + imageTagKey = "imageTag" + versionKey = "version" + // NodeVersionsMappingKind defines kind of NodeVersionMappingKind + NodeVersionsMappingKind types.Kind = "NodeVersionsMapping" +) + +// Init Initializes the transformer +func (t *NodejsDockerfileGenerator) Init(tc transformertypes.Transformer, env *environment.Environment) error { + t.Config = tc + t.Env = env + + // load the config + t.NodejsConfig = &NodejsDockerfileYamlConfig{} + if err := common.GetObjFromInterface(t.Config.Spec.Config, t.NodejsConfig); err != nil { + return fmt.Errorf("unable to load config for Transformer %+v into %T . Error: %q", t.Config.Spec.Config, t.NodejsConfig, err) + } + if t.NodejsConfig.DefaultPackageManager == "" { + t.NodejsConfig.DefaultPackageManager = defaultPackageManager + } + + // load the version mapping file + mappingFilePath := filepath.Join(t.Env.GetEnvironmentContext(), versionMappingFilePath) + spec, err := LoadNodeVersionMappingsFile(mappingFilePath) + if err != nil { + return fmt.Errorf("failed to load the node version mappings file at path %s . Error: %q", versionMappingFilePath, err) + } + t.Spec = spec + if t.NodejsConfig.DefaultNodejsVersion == "" { + if len(t.Spec.NodeVersions) != 0 { + if _, ok := t.Spec.NodeVersions[0][versionKey]; ok { + t.NodejsConfig.DefaultNodejsVersion = t.Spec.NodeVersions[0][versionKey] + } + } + } + logrus.Debugf("Extracted node versions from nodeversion mappings file - %+v", t.Spec) + return nil +} + +// GetConfig returns the transformer config +func (t *NodejsDockerfileGenerator) GetConfig() (transformertypes.Transformer, *environment.Environment) { + return t.Config, t.Env +} + +// DirectoryDetect runs detect in each sub directory +func (t *NodejsDockerfileGenerator) DirectoryDetect(dir string) (map[string][]transformertypes.Artifact, error) { + packageJsonPath := filepath.Join(dir, packageJSONFile) + packageJson := PackageJSON{} + if err := common.ReadJSON(packageJsonPath, &packageJson); err != nil { + logrus.Debugf("failed to read the package.json file at the path %s . Error: %q", packageJsonPath, err) + return nil, nil + } + if packageJson.Name == "" { + return nil, fmt.Errorf("unable to get name of nodejs service at %s. Ignoring", dir) + } + serviceName := packageJson.Name + normalizedServiceName := common.MakeStringK8sServiceNameCompliant(serviceName) + services := map[string][]transformertypes.Artifact{ + normalizedServiceName: {{ + Paths: map[transformertypes.PathType][]string{artifacts.ServiceDirPathType: {dir}}, + Configs: map[transformertypes.ConfigType]interface{}{ + artifacts.OriginalNameConfigType: artifacts.OriginalNameConfig{OriginalName: serviceName}, + }, + }}, + } + return services, nil +} + +// Transform transforms the artifacts +func (t *NodejsDockerfileGenerator) Transform(newArtifacts []transformertypes.Artifact, alreadySeenArtifacts []transformertypes.Artifact) ([]transformertypes.PathMapping, []transformertypes.Artifact, error) { + pathMappings := []transformertypes.PathMapping{} + artifactsCreated := []transformertypes.Artifact{} + for _, newArtifact := range newArtifacts { + if len(newArtifact.Paths[artifacts.ServiceDirPathType]) == 0 { + continue + } + serviceDir := newArtifact.Paths[artifacts.ServiceDirPathType][0] + relSrcPath, err := filepath.Rel(t.Env.GetEnvironmentSource(), serviceDir) + if err != nil { + logrus.Errorf("Unable to convert source path %s to be relative. Error: %q", serviceDir, err) + } + serviceConfig := artifacts.ServiceConfig{} + if err := newArtifact.GetConfig(artifacts.ServiceConfigType, &serviceConfig); err != nil { + logrus.Errorf("unable to load config for Transformer into %T . Error: %q", serviceConfig, err) + continue + } + imageName := artifacts.ImageName{} + if err := newArtifact.GetConfig(artifacts.ImageNameConfigType, &imageName); err != nil { + logrus.Debugf("unable to load config for Transformer into %T . Error: %q", imageName, err) + } + if imageName.ImageName == "" { + imageName.ImageName = common.MakeStringContainerImageNameCompliant(serviceConfig.ServiceName) + } + //TODO: WASI + //ir := irtypes.IR{} + //irPresent := true + //if err := newArtifact.GetConfig(irtypes.IRConfigType, &ir); err != nil { + // irPresent = false + // logrus.Debugf("unable to load config for Transformer into %T . Error: %q", ir, err) + //} + build := false + packageJSON := PackageJSON{} + packageJsonPath := filepath.Join(serviceDir, packageJSONFile) + if err := common.ReadJSON(packageJsonPath, &packageJSON); err != nil { + logrus.Errorf("failed to parse the package.json file at path %s . Error: %q", packageJsonPath, err) + continue + } + if _, ok := packageJSON.Scripts["build"]; ok { + build = true + } + nodeVersion := t.NodejsConfig.DefaultNodejsVersion + if nodeVersionConstraint, ok := packageJSON.Engines["node"]; ok { + nodeVersion = getNodeVersion( + nodeVersionConstraint, + t.NodejsConfig.DefaultNodejsVersion, + common.Map(t.Spec.NodeVersions, func(x map[string]string) string { return x[versionKey] }), + ) + logrus.Debugf("Selected nodeVersion is - %s", nodeVersion) + } + packageManager := t.NodejsConfig.DefaultPackageManager + if packageJSON.PackageManager != "" { + parts := strings.Split(packageJSON.PackageManager, "@") + if len(parts) > 0 { + packageManager = parts[0] + } + } + //TODO: WASI + //ports := ir.GetAllServicePorts() + //if len(ports) == 0 { + // envPath := filepath.Join(serviceDir, ".env") + // envMap, err := godotenv.Read(envPath) + // if err != nil { + // if !os.IsNotExist(err) { + // logrus.Warnf("failed to parse the .env file at the path %s . Error: %q", envPath, err) + // } + // } else if portString, ok := envMap["PORT"]; ok { + // port, err := cast.ToInt32E(portString) + // if err != nil { + // logrus.Errorf("failed to parse the port string '%s' as an integer. Error: %q", portString, err) + // } else { + // ports = []int32{port} + // } + // } + //} + //port := commonqa.GetPortForService(ports, `"`+newArtifact.Name+`"`) + port := int32(9003) + var props map[string]string + if idx := common.FindIndex(t.Spec.NodeVersions, func(x map[string]string) bool { return x[versionKey] == nodeVersion }); idx != -1 { + props = t.Spec.NodeVersions[idx] + } + nodejsConfig := NodejsTemplateConfig{ + Build: build, + Port: port, + NodeVersion: nodeVersion, + // NodeImageTag: getNodeImageTag(t.Spec.NodeVersions, nodeVersion), // To use this, change the base image in the Dockerfile template to- FROM node:{{ .NodeImageTag }} + NodeMajorVersion: strings.TrimPrefix(semver.Major(nodeVersion), "v"), + NodeVersionProperties: props, + PackageManager: packageManager, + } + pathMappings = append(pathMappings, transformertypes.PathMapping{ + Type: transformertypes.SourcePathMappingType, + DestPath: common.DefaultSourceDir, + }, transformertypes.PathMapping{ + Type: transformertypes.TemplatePathMappingType, + SrcPath: filepath.Join(t.Env.Context, t.Config.Spec.TemplatesDir), + DestPath: filepath.Join(common.DefaultSourceDir, relSrcPath), + TemplateConfig: nodejsConfig, + }) + paths := newArtifact.Paths + paths[artifacts.DockerfilePathType] = []string{filepath.Join(common.DefaultSourceDir, relSrcPath, common.DefaultDockerfileName)} + p := transformertypes.Artifact{ + Name: imageName.ImageName, + Type: artifacts.DockerfileArtifactType, + Paths: paths, + Configs: map[transformertypes.ConfigType]interface{}{ + artifacts.ImageNameConfigType: imageName, + }, + } + dfs := transformertypes.Artifact{ + Name: serviceConfig.ServiceName, + Type: artifacts.DockerfileForServiceArtifactType, + Paths: newArtifact.Paths, + Configs: map[transformertypes.ConfigType]interface{}{ + artifacts.ImageNameConfigType: imageName, + artifacts.ServiceConfigType: serviceConfig, + }, + } + //TODO: WASI + //if irPresent { + // dfs.Configs[irtypes.IRConfigType] = ir + //} + artifactsCreated = append(artifactsCreated, p, dfs) + } + return pathMappings, artifactsCreated, nil +} + +// LoadNodeVersionMappingsFile loads the node version mappings file +func LoadNodeVersionMappingsFile(mappingFilePath string) (NodeVersionsMappingSpec, error) { + mappingFile := NodeVersionsMapping{} + if err := common.ReadMove2KubeYaml(mappingFilePath, &mappingFile); err != nil { + return mappingFile.Spec, fmt.Errorf("failed to load the Node versions mapping file at path %s . Error: %q", mappingFilePath, err) + } + // validate the file + if len(mappingFile.Spec.NodeVersions) == 0 { + return mappingFile.Spec, fmt.Errorf("the node version mappings file at path %s is invalid. Atleast one node version should be specified", mappingFilePath) + } + for i, v := range mappingFile.Spec.NodeVersions { + if _, ok := v[versionKey]; !ok { + return mappingFile.Spec, fmt.Errorf("the version is missing from the object %#v at the %dth index in the array", mappingFile.Spec.NodeVersions[i], i) + } + } + // sort the list using semantic version comparison + if !mappingFile.Spec.DisableSort { + sort.SliceStable(mappingFile.Spec.NodeVersions, func(i, j int) bool { + return semver.Compare(mappingFile.Spec.NodeVersions[i][versionKey], mappingFile.Spec.NodeVersions[j][versionKey]) == 1 + }) + } + return mappingFile.Spec, nil +} diff --git a/transformer/dockerfilegenerator/phpdockerfiletransformer.go b/transformer/dockerfilegenerator/phpdockerfiletransformer.go new file mode 100644 index 0000000..00dd830 --- /dev/null +++ b/transformer/dockerfilegenerator/phpdockerfiletransformer.go @@ -0,0 +1,264 @@ +/* + * Copyright IBM Corporation 2021 + * + * 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 dockerfilegenerator + +import ( + "fmt" + "os" + "path/filepath" + "strconv" + "strings" + + gopache "github.com/Akash-Nayak/GopacheConfig" + "github.com/konveyor/move2kube-wasm/common" + "github.com/konveyor/move2kube-wasm/environment" + //"github.com/konveyor/move2kube-wasm/qaengine" + //irtypes "github.com/konveyor/move2kube-wasm/types/ir" + //"github.com/konveyor/move2kube-wasm/types/qaengine/commonqa" + transformertypes "github.com/konveyor/move2kube-wasm/types/transformer" + "github.com/konveyor/move2kube-wasm/types/transformer/artifacts" + "github.com/sirupsen/logrus" +) + +const ( + phpExt = ".php" + virtualHost = "VirtualHost" + confExt = ".conf" +) + +// PHPDockerfileGenerator implements the Transformer interface +type PHPDockerfileGenerator struct { + Config transformertypes.Transformer + Env *environment.Environment +} + +// PhpTemplateConfig implements Php config interface +type PhpTemplateConfig struct { + ConfFile string + ConfFilePort int32 +} + +// Init Initializes the transformer +func (t *PHPDockerfileGenerator) Init(tc transformertypes.Transformer, env *environment.Environment) (err error) { + t.Config = tc + t.Env = env + return nil +} + +// GetConfig returns the transformer config +func (t *PHPDockerfileGenerator) GetConfig() (transformertypes.Transformer, *environment.Environment) { + return t.Config, t.Env +} + +// parseConfFile parses the conf file to detect the port +func parseConfFile(confFilePath string) (int32, error) { + var port int32 + confFile, err := os.Open(confFilePath) + if err != nil { + logrus.Errorf("Could not open the apache config file: %s", err) + return port, err + } + defer confFile.Close() + root, err := gopache.Parse(confFile) + if err != nil { + logrus.Errorf("Error while parsing apache config file : %s", err) + return port, err + } + match, err := root.FindOne(virtualHost) + if err != nil { + logrus.Debugf("Could not find the VirtualHost in apache config file: %s", err) + return port, err + } + tokens := strings.Split(match.Content, ":") + if len(tokens) > 1 { + detectedPort, err := strconv.ParseInt(tokens[1], 10, 32) + if err != nil { + logrus.Errorf("Error while converting the port from string to int : %s", err) + return port, err + } + return int32(detectedPort), nil + } + return port, err +} + +// detectConfFiles detects if conf files are present or not +func detectConfFiles(dir string) ([]string, error) { + var confFilesPaths []string + confFiles, err := common.GetFilesByExt(dir, []string{confExt}) + if err != nil { + logrus.Debugf("Could not find conf files %s", err) + return confFilesPaths, err + } + for _, confFilePath := range confFiles { + confFile, err := os.Open(confFilePath) + if err != nil { + logrus.Debugf("Could not open the conf file: %s", err) + confFile.Close() + continue + } + defer confFile.Close() + _, err = gopache.Parse(confFile) + if err != nil { + logrus.Debugf("Error while parsing conf file : %s", err) + continue + } + confFileRelPath, err := filepath.Rel(dir, confFilePath) + if err != nil { + logrus.Errorf("Unable to resolve apache config file path %s as rel path : %s", confFilePath, err) + continue + } + confFilesPaths = append(confFilesPaths, confFileRelPath) + } + return confFilesPaths, nil +} + +// GetConfFileForService returns ports used by a service +func GetConfFileForService(confFiles []string, serviceName string) string { + noAnswer := "none of the above" + confFiles = append(confFiles, noAnswer) + //TODO: WASI + //quesKey := common.JoinQASubKeys(common.ConfigServicesKey, `"`+serviceName+`"`, common.ConfigApacheConfFileForServiceKeySegment) + //desc := fmt.Sprintf("Choose the apache config file to be used for the service %s", serviceName) + //hints := []string{fmt.Sprintf("Selected apache config file will be used for identifying the port to be exposed for the service %s", serviceName)} + //selectedConfFile := qaengine.FetchSelectAnswer(quesKey, desc, hints, confFiles[0], confFiles, nil) + var selectedConfFile string + if selectedConfFile == noAnswer { + logrus.Debugf("No apache config file selected for the service %s", serviceName) + return "" + } + return selectedConfFile +} + +// DirectoryDetect runs detect in each sub directory +func (t *PHPDockerfileGenerator) DirectoryDetect(dir string) (map[string][]transformertypes.Artifact, error) { + phpFiles, err := common.GetFilesByExtInCurrDir(dir, []string{phpExt}) + if err != nil { + return nil, fmt.Errorf("failed to look for .php files in the directory %s . Error: %q", dir, err) + } + if len(phpFiles) == 0 { + return nil, nil + } + serviceName := filepath.Base(dir) + normalizedServiceName := common.MakeStringK8sServiceNameCompliant(serviceName) + services := map[string][]transformertypes.Artifact{ + normalizedServiceName: {{ + Paths: map[transformertypes.PathType][]string{ + artifacts.ServiceDirPathType: {dir}, + }, + Configs: map[transformertypes.ConfigType]interface{}{ + artifacts.OriginalNameConfigType: artifacts.OriginalNameConfig{OriginalName: serviceName}, + }, + }}, + } + return services, nil +} + +// Transform transforms the artifacts +func (t *PHPDockerfileGenerator) Transform(newArtifacts []transformertypes.Artifact, alreadySeenArtifacts []transformertypes.Artifact) ([]transformertypes.PathMapping, []transformertypes.Artifact, error) { + pathMappings := []transformertypes.PathMapping{} + artifactsCreated := []transformertypes.Artifact{} + for _, a := range newArtifacts { + if len(a.Paths[artifacts.ServiceDirPathType]) == 0 { + continue + } + relSrcPath, err := filepath.Rel(t.Env.GetEnvironmentSource(), a.Paths[artifacts.ServiceDirPathType][0]) + if err != nil { + logrus.Errorf("Unable to convert source path %s to be relative : %s", a.Paths[artifacts.ServiceDirPathType][0], err) + } + var sConfig artifacts.ServiceConfig + err = a.GetConfig(artifacts.ServiceConfigType, &sConfig) + if err != nil { + logrus.Errorf("unable to load config for Transformer into %T : %s", sConfig, err) + continue + } + sImageName := artifacts.ImageName{} + err = a.GetConfig(artifacts.ImageNameConfigType, &sImageName) + if err != nil { + logrus.Debugf("unable to load config for Transformer into %T : %s", sImageName, err) + } + //TODO: WASI + + //ir := irtypes.IR{} + //irPresent := true + //err = a.GetConfig(irtypes.IRConfigType, &ir) + //if err != nil { + // irPresent = false + // logrus.Debugf("unable to load config for Transformer into %T : %s", ir, err) + //} + //detectedPorts := ir.GetAllServicePorts() + var phpConfig PhpTemplateConfig + confFiles, err := detectConfFiles(a.Paths[artifacts.ServiceDirPathType][0]) + if err != nil { + logrus.Debugf("Could not detect any conf files %s", err) + } else { + if len(confFiles) == 1 { + phpConfig.ConfFile = confFiles[0] + } else if len(confFiles) > 1 { + phpConfig.ConfFile = GetConfFileForService(confFiles, a.Name) + } + if phpConfig.ConfFile != "" { + phpConfig.ConfFilePort, err = parseConfFile(filepath.Join(a.Paths[artifacts.ServiceDirPathType][0], phpConfig.ConfFile)) + if err != nil { + logrus.Errorf("Error while parsing configuration file : %s", err) + } + } + if phpConfig.ConfFilePort == 0 { + //TODO: WASI + + //phpConfig.ConfFilePort = commonqa.GetPortForService(detectedPorts, `"`+a.Name+`"`) + } + } + if sImageName.ImageName == "" { + sImageName.ImageName = common.MakeStringContainerImageNameCompliant(sConfig.ServiceName) + } + pathMappings = append(pathMappings, transformertypes.PathMapping{ + Type: transformertypes.SourcePathMappingType, + DestPath: common.DefaultSourceDir, + }, transformertypes.PathMapping{ + Type: transformertypes.TemplatePathMappingType, + SrcPath: filepath.Join(t.Env.Context, t.Config.Spec.TemplatesDir), + DestPath: filepath.Join(common.DefaultSourceDir, relSrcPath), + TemplateConfig: phpConfig, + }) + paths := a.Paths + paths[artifacts.DockerfilePathType] = []string{filepath.Join(common.DefaultSourceDir, relSrcPath, common.DefaultDockerfileName)} + p := transformertypes.Artifact{ + Name: sImageName.ImageName, + Type: artifacts.DockerfileArtifactType, + Paths: paths, + Configs: map[transformertypes.ConfigType]interface{}{ + artifacts.ImageNameConfigType: sImageName, + }, + } + dfs := transformertypes.Artifact{ + Name: sConfig.ServiceName, + Type: artifacts.DockerfileForServiceArtifactType, + Paths: a.Paths, + Configs: map[transformertypes.ConfigType]interface{}{ + artifacts.ImageNameConfigType: sImageName, + artifacts.ServiceConfigType: sConfig, + }, + } + //TODO: WASI + + //if irPresent { + // dfs.Configs[irtypes.IRConfigType] = ir + //} + artifactsCreated = append(artifactsCreated, p, dfs) + } + return pathMappings, artifactsCreated, nil +} diff --git a/transformer/dockerfilegenerator/pythondockerfiletransformer.go b/transformer/dockerfilegenerator/pythondockerfiletransformer.go new file mode 100644 index 0000000..30df938 --- /dev/null +++ b/transformer/dockerfilegenerator/pythondockerfiletransformer.go @@ -0,0 +1,296 @@ +/* + * Copyright IBM Corporation 2021 + * + * 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 dockerfilegenerator + +import ( + "bufio" + "fmt" + "os" + "path/filepath" + "regexp" + + "github.com/konveyor/move2kube-wasm/common" + "github.com/konveyor/move2kube-wasm/environment" + //"github.com/konveyor/move2kube/qaengine" + //irtypes "github.com/konveyor/move2kube/types/ir" + //"github.com/konveyor/move2kube/types/qaengine/commonqa" + transformertypes "github.com/konveyor/move2kube-wasm/types/transformer" + "github.com/konveyor/move2kube-wasm/types/transformer/artifacts" + "github.com/sirupsen/logrus" +) + +// PythonDockerfileGenerator implements the Transformer interface +type PythonDockerfileGenerator struct { + Config transformertypes.Transformer + Env *environment.Environment +} + +// PythonTemplateConfig implements python config interface +type PythonTemplateConfig struct { + AppName string + Port int32 + StartingScriptRelPath string + RequirementsTxt string + IsDjango bool +} + +// PythonConfig implements python config interface +type PythonConfig struct { + IsDjango bool `json:"IsDjango" yaml:"IsDjango"` +} + +const ( + pythonExt = ".py" + requirementsTxtFile = "requirements.txt" + //RequirementsTxtPathType points to the requirements.txt file if it's present + RequirementsTxtPathType transformertypes.PathType = "RequirementsTxtPathType" + // PythonServiceConfigType points to python config + PythonServiceConfigType transformertypes.ConfigType = "PythonConfig" + // MainPythonFilesPathType points to the .py files path which contain main function + MainPythonFilesPathType transformertypes.PathType = "MainPythonFilesPathType" + // PythonFilesPathType points to the .py files path + PythonFilesPathType transformertypes.PathType = "PythonFilesPathType" +) + +var ( + djangoRegex = regexp.MustCompile(`(?m)^[Dd]jango`) + pythonMainRegex = regexp.MustCompile(`^if\s+__name__\s*==\s*['"]__main__['"]\s*:\s*$`) +) + +// Init Initializes the transformer +func (t *PythonDockerfileGenerator) Init(tc transformertypes.Transformer, env *environment.Environment) (err error) { + t.Config = tc + t.Env = env + return nil +} + +// GetConfig returns the transformer config +func (t *PythonDockerfileGenerator) GetConfig() (transformertypes.Transformer, *environment.Environment) { + return t.Config, t.Env +} + +// findMainScripts returns the path of .py files having the main function +func findMainScripts(pythonFilesPath []string) ([]string, error) { + if len(pythonFilesPath) == 0 { + return nil, nil + } + pythonMainFiles := []string{} + for _, pythonFilePath := range pythonFilesPath { + pythonFile, err := os.Open(pythonFilePath) + if err != nil { + logrus.Debugf("failed to open the file at path %s . Error: %q", pythonFilePath, err) + continue + } + defer pythonFile.Close() + scanner := bufio.NewScanner(pythonFile) + scanner.Split(bufio.ScanLines) + for scanner.Scan() { + if pythonMainRegex.MatchString(scanner.Text()) { + pythonMainFiles = append(pythonMainFiles, pythonFilePath) + break + } + } + pythonFile.Close() + } + return pythonMainFiles, nil +} + +// findDjangoDependency checks for django dependency in the requirements.txt file +func findDjangoDependency(reqTxtFilePath string) bool { + reqTxtFile, err := os.ReadFile(reqTxtFilePath) + if err != nil { + logrus.Warnf("failed to read the file at path %s . Error: %q", reqTxtFilePath, err) + return false + } + return djangoRegex.MatchString(string(reqTxtFile)) +} + +// getMainPythonFileForService returns the main file used by a service +func getMainPythonFileForService(mainPythonFilesPath []string, baseDir string, serviceName string) string { + var mainPythonFilesRelPath []string + for _, mainPythonFilePath := range mainPythonFilesPath { + if mainPythonFileRelPath, err := filepath.Rel(baseDir, mainPythonFilePath); err == nil { + mainPythonFilesRelPath = append(mainPythonFilesRelPath, mainPythonFileRelPath) + } + } + //TODO: WASI + + //quesKey := common.JoinQASubKeys(common.ConfigServicesKey, `"`+serviceName+`"`, common.ConfigMainPythonFileForServiceKeySegment) + //desc := fmt.Sprintf("Select the main file to be used for the service %s :", serviceName) + //hints := []string{fmt.Sprintf("Selected main file will be used for the service %s", serviceName)} + //return qaengine.FetchSelectAnswer(quesKey, desc, hints, mainPythonFilesRelPath[0], mainPythonFilesRelPath, nil) + return "" +} + +// getStartingPythonFileForService returns the starting python file used by a service +func getStartingPythonFileForService(pythonFilesPath []string, baseDir string, serviceName string) string { + var pythonFilesRelPath []string + for _, pythonFilePath := range pythonFilesPath { + if pythonFileRelPath, err := filepath.Rel(baseDir, pythonFilePath); err == nil { + pythonFilesRelPath = append(pythonFilesRelPath, pythonFileRelPath) + } + } + //TODO: WASI + + //quesKey := common.JoinQASubKeys(common.ConfigServicesKey, `"`+serviceName+`"`, common.ConfigStartingPythonFileForServiceKeySegment) + //desc := fmt.Sprintf("Select the python file to be used for the service %s :", serviceName) + //hints := []string{fmt.Sprintf("Selected python file will be used for starting the service %s", serviceName)} + //return qaengine.FetchSelectAnswer(quesKey, desc, hints, pythonFilesRelPath[0], pythonFilesRelPath, nil) + return "" +} + +// DirectoryDetect runs detect in each sub directory +func (t *PythonDockerfileGenerator) DirectoryDetect(dir string) (map[string][]transformertypes.Artifact, error) { + pythonFilesPath, err := common.GetFilesByExtInCurrDir(dir, []string{pythonExt}) + if err != nil { + return nil, fmt.Errorf("failed to look for python files in the directory %s . Error: %q", dir, err) + } + if len(pythonFilesPath) == 0 { + return nil, nil + } + mainPythonFilesPath, err := findMainScripts(pythonFilesPath) + if err != nil { + logrus.Debugf("failed to find the python files in the project at directory %s that have a main function. Error: %q", dir, err) + } + serviceName := filepath.Base(dir) + normalizedServiceName := common.MakeStringK8sServiceNameCompliant(serviceName) + pythonService := transformertypes.Artifact{ + Paths: map[transformertypes.PathType][]string{ + artifacts.ServiceDirPathType: {dir}, + MainPythonFilesPathType: mainPythonFilesPath, + PythonFilesPathType: pythonFilesPath, + }, Configs: map[transformertypes.ConfigType]interface{}{ + artifacts.OriginalNameConfigType: artifacts.OriginalNameConfig{OriginalName: serviceName}, + }, + } + + // check if it is a Django project + pythonService.Configs[PythonServiceConfigType] = PythonConfig{IsDjango: false} + requirementsTxtFiles, err := common.GetFilesInCurrentDirectory(dir, []string{requirementsTxtFile}, nil) + if err != nil { + return nil, fmt.Errorf("failed to look for the requirements.txt file in the directory %s . Error: %q", dir, err) + } + if len(requirementsTxtFiles) > 0 { + requirementsTxtPath := requirementsTxtFiles[0] + pythonService.Paths[RequirementsTxtPathType] = []string{requirementsTxtPath} + pythonService.Configs[PythonServiceConfigType] = PythonConfig{IsDjango: findDjangoDependency(requirementsTxtPath)} + } + return map[string][]transformertypes.Artifact{normalizedServiceName: {pythonService}}, nil +} + +// Transform transforms the artifacts +func (t *PythonDockerfileGenerator) Transform(newArtifacts []transformertypes.Artifact, alreadySeenArtifacts []transformertypes.Artifact) ([]transformertypes.PathMapping, []transformertypes.Artifact, error) { + pathMappings := []transformertypes.PathMapping{} + artifactsCreated := []transformertypes.Artifact{} + for _, newArtifact := range newArtifacts { + if len(newArtifact.Paths[artifacts.ServiceDirPathType]) == 0 { + logrus.Errorf("the service directory is missing from the artifact: %+v", newArtifact) + continue + } + serviceDir := newArtifact.Paths[artifacts.ServiceDirPathType][0] + relSrcPath, err := filepath.Rel(t.Env.GetEnvironmentSource(), serviceDir) + if err != nil { + logrus.Errorf("Unable to convert source path %s to be relative : %s", serviceDir, err) + continue + } + serviceConfig := artifacts.ServiceConfig{} + if err := newArtifact.GetConfig(artifacts.ServiceConfigType, &serviceConfig); err != nil { + logrus.Errorf("unable to load config for Transformer into %T : %s", serviceConfig, err) + continue + } + imageName := artifacts.ImageName{} + if err := newArtifact.GetConfig(artifacts.ImageNameConfigType, &imageName); err != nil { + logrus.Debugf("unable to load config for Transformer into %T : %s", imageName, err) + } + if imageName.ImageName == "" { + imageName.ImageName = common.MakeStringContainerImageNameCompliant(serviceConfig.ServiceName) + } + //TODO: WASI + + //ir := irtypes.IR{} + //irPresent := true + //if err := newArtifact.GetConfig(irtypes.IRConfigType, &ir); err != nil { + // irPresent = false + // logrus.Debugf("unable to load config for Transformer into %T : %s", ir, err) + //} + var pythonTemplateConfig PythonTemplateConfig + if len(newArtifact.Paths[MainPythonFilesPathType]) > 0 { + pythonTemplateConfig.StartingScriptRelPath = getMainPythonFileForService(newArtifact.Paths[MainPythonFilesPathType], serviceDir, newArtifact.Name) + } else { + pythonTemplateConfig.StartingScriptRelPath = getStartingPythonFileForService(newArtifact.Paths[PythonFilesPathType], serviceDir, newArtifact.Name) + } + pythonTemplateConfig.AppName = newArtifact.Name + var pythonConfig PythonConfig + err = newArtifact.GetConfig(PythonServiceConfigType, &pythonConfig) + if err != nil { + logrus.Debugf("unable to load config for Transformer into %T : %s", imageName, err) + } + pythonTemplateConfig.IsDjango = pythonConfig.IsDjango + //TODO: WASI + + //ports := ir.GetAllServicePorts() + //if len(ports) == 0 { + // ports = []int32{common.DefaultServicePort} + //} + //pythonTemplateConfig.Port = commonqa.GetPortForService(ports, `"`+newArtifact.Name+`"`) + if len(newArtifact.Paths[artifacts.ServiceDirPathType]) == 0 { + logrus.Errorf("The service directory path is missing for the artifact: %+v", newArtifact) + continue + } + if len(newArtifact.Paths[RequirementsTxtPathType]) != 0 { + if requirementsTxt, err := filepath.Rel(serviceDir, newArtifact.Paths[RequirementsTxtPathType][0]); err == nil { + pythonTemplateConfig.RequirementsTxt = requirementsTxt + } + } + pathMappings = append(pathMappings, transformertypes.PathMapping{ + Type: transformertypes.SourcePathMappingType, + DestPath: common.DefaultSourceDir, + }, transformertypes.PathMapping{ + Type: transformertypes.TemplatePathMappingType, + SrcPath: filepath.Join(t.Env.Context, t.Config.Spec.TemplatesDir), + DestPath: filepath.Join(common.DefaultSourceDir, relSrcPath), + TemplateConfig: pythonTemplateConfig, + }) + paths := newArtifact.Paths + paths[artifacts.DockerfilePathType] = []string{filepath.Join(common.DefaultSourceDir, relSrcPath, common.DefaultDockerfileName)} + p := transformertypes.Artifact{ + Name: imageName.ImageName, + Type: artifacts.DockerfileArtifactType, + Paths: paths, + Configs: map[transformertypes.ConfigType]interface{}{ + artifacts.ServiceConfigType: serviceConfig, + artifacts.ImageNameConfigType: imageName, + }, + } + dfs := transformertypes.Artifact{ + Name: serviceConfig.ServiceName, + Type: artifacts.DockerfileForServiceArtifactType, + Paths: newArtifact.Paths, + Configs: map[transformertypes.ConfigType]interface{}{ + artifacts.ServiceConfigType: serviceConfig, + artifacts.ImageNameConfigType: imageName, + }, + } + //TODO: WASI + + //if irPresent { + // dfs.Configs[irtypes.IRConfigType] = ir + //} + artifactsCreated = append(artifactsCreated, p, dfs) + } + return pathMappings, artifactsCreated, nil +} diff --git a/transformer/dockerfilegenerator/rubydockerfiletransformer.go b/transformer/dockerfilegenerator/rubydockerfiletransformer.go new file mode 100644 index 0000000..8a427b6 --- /dev/null +++ b/transformer/dockerfilegenerator/rubydockerfiletransformer.go @@ -0,0 +1,167 @@ +/* + * Copyright IBM Corporation 2021 + * + * 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 dockerfilegenerator + +import ( + "fmt" + "path/filepath" + + "github.com/konveyor/move2kube-wasm/common" + "github.com/konveyor/move2kube-wasm/environment" + //irtypes "github.com/konveyor/move2kube/types/ir" + //"github.com/konveyor/move2kube/types/qaengine/commonqa" + transformertypes "github.com/konveyor/move2kube-wasm/types/transformer" + "github.com/konveyor/move2kube-wasm/types/transformer/artifacts" + "github.com/sirupsen/logrus" +) + +const ( + rubyFileExt = ".rb" +) + +// RubyDockerfileGenerator implements the Transformer interface +type RubyDockerfileGenerator struct { + Config transformertypes.Transformer + Env *environment.Environment +} + +// RubyTemplateConfig implements Ruby config interface +type RubyTemplateConfig struct { + Port int32 + AppName string +} + +// Init Initializes the transformer +func (t *RubyDockerfileGenerator) Init(tc transformertypes.Transformer, env *environment.Environment) (err error) { + t.Config = tc + t.Env = env + return nil +} + +// GetConfig returns the transformer config +func (t *RubyDockerfileGenerator) GetConfig() (transformertypes.Transformer, *environment.Environment) { + return t.Config, t.Env +} + +// DirectoryDetect runs detect in each sub directory +func (t *RubyDockerfileGenerator) DirectoryDetect(dir string) (map[string][]transformertypes.Artifact, error) { + gemfilePaths, err := common.GetFilesByName(dir, []string{"Gemfile"}, nil) + if err != nil { + return nil, fmt.Errorf("failed to look for Gemfiles in the dir %s . Error: %q", dir, err) + } + if len(gemfilePaths) == 0 { + return nil, nil + } + rubyFiles, err := common.GetFilesByExt(dir, []string{rubyFileExt}) + if err != nil { + return nil, fmt.Errorf("failed to look for ruby files in the directory %s . Error: %q", dir, err) + } + if len(rubyFiles) == 0 { + return nil, fmt.Errorf("found a Gemfile, but didn't find any ruby files in the directory %s", dir) + } + serviceName := filepath.Base(dir) + normalizedServiceName := common.MakeStringK8sServiceNameCompliant(serviceName) + services := map[string][]transformertypes.Artifact{ + normalizedServiceName: {{ + Paths: map[transformertypes.PathType][]string{ + artifacts.ServiceDirPathType: {dir}, + }, + Configs: map[transformertypes.ConfigType]interface{}{ + artifacts.OriginalNameConfigType: artifacts.OriginalNameConfig{OriginalName: serviceName}, + }, + }}, + } + return services, nil +} + +// Transform transforms the artifacts +func (t *RubyDockerfileGenerator) Transform(newArtifacts []transformertypes.Artifact, alreadySeenArtifacts []transformertypes.Artifact) ([]transformertypes.PathMapping, []transformertypes.Artifact, error) { + pathMappings := []transformertypes.PathMapping{} + artifactsCreated := []transformertypes.Artifact{} + for _, a := range newArtifacts { + if len(a.Paths[artifacts.ServiceDirPathType]) == 0 { + continue + } + relSrcPath, err := filepath.Rel(t.Env.GetEnvironmentSource(), a.Paths[artifacts.ServiceDirPathType][0]) + if err != nil { + logrus.Errorf("Unable to convert source path %s to be relative : %s", a.Paths[artifacts.ServiceDirPathType][0], err) + } + var sConfig artifacts.ServiceConfig + err = a.GetConfig(artifacts.ServiceConfigType, &sConfig) + if err != nil { + logrus.Errorf("unable to load config for Transformer into %T : %s", sConfig, err) + continue + } + sImageName := artifacts.ImageName{} + err = a.GetConfig(artifacts.ImageNameConfigType, &sImageName) + if err != nil { + logrus.Debugf("unable to load config for Transformer into %T : %s", sImageName, err) + } + //TODO: WASI + + //ir := irtypes.IR{} + //irPresent := true + //err = a.GetConfig(irtypes.IRConfigType, &ir) + //if err != nil { + // irPresent = false + // logrus.Debugf("unable to load config for Transformer into %T : %s", ir, err) + //} + var rubyConfig RubyTemplateConfig + //detectedPorts := ir.GetAllServicePorts() + //if len(detectedPorts) == 0 { + // detectedPorts = append(detectedPorts, common.DefaultServicePort) + //} + //rubyConfig.Port = commonqa.GetPortForService(detectedPorts, `"`+a.Name+`"`) + //rubyConfig.AppName = a.Name + if sImageName.ImageName == "" { + sImageName.ImageName = common.MakeStringContainerImageNameCompliant(sConfig.ServiceName) + } + pathMappings = append(pathMappings, transformertypes.PathMapping{ + Type: transformertypes.SourcePathMappingType, + DestPath: common.DefaultSourceDir, + }, transformertypes.PathMapping{ + Type: transformertypes.TemplatePathMappingType, + SrcPath: filepath.Join(t.Env.Context, t.Config.Spec.TemplatesDir), + DestPath: filepath.Join(common.DefaultSourceDir, relSrcPath), + TemplateConfig: rubyConfig, + }) + paths := a.Paths + paths[artifacts.DockerfilePathType] = []string{filepath.Join(common.DefaultSourceDir, relSrcPath, common.DefaultDockerfileName)} + p := transformertypes.Artifact{ + Name: sImageName.ImageName, + Type: artifacts.DockerfileArtifactType, + Paths: paths, + Configs: map[transformertypes.ConfigType]interface{}{ + artifacts.ImageNameConfigType: sImageName, + }, + } + dfs := transformertypes.Artifact{ + Name: sConfig.ServiceName, + Type: artifacts.DockerfileForServiceArtifactType, + Paths: a.Paths, + Configs: map[transformertypes.ConfigType]interface{}{ + artifacts.ImageNameConfigType: sImageName, + artifacts.ServiceConfigType: sConfig, + }, + } + //if irPresent { + // dfs.Configs[irtypes.IRConfigType] = ir + //} + artifactsCreated = append(artifactsCreated, p, dfs) + } + return pathMappings, artifactsCreated, nil +} diff --git a/transformer/dockerfilegenerator/rustdockerfiletransformer.go b/transformer/dockerfilegenerator/rustdockerfiletransformer.go new file mode 100644 index 0000000..7a25867 --- /dev/null +++ b/transformer/dockerfilegenerator/rustdockerfiletransformer.go @@ -0,0 +1,200 @@ +/* + * Copyright IBM Corporation 2021 + * + * 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 dockerfilegenerator + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/BurntSushi/toml" + "github.com/konveyor/move2kube-wasm/common" + "github.com/konveyor/move2kube-wasm/environment" + //irtypes "github.com/konveyor/move2kube/types/ir" + //"github.com/konveyor/move2kube/types/qaengine/commonqa" + transformertypes "github.com/konveyor/move2kube-wasm/types/transformer" + "github.com/konveyor/move2kube-wasm/types/transformer/artifacts" + "github.com/sirupsen/logrus" +) + +// RustDockerfileGenerator implements the Transformer interface +type RustDockerfileGenerator struct { + Config transformertypes.Transformer + Env *environment.Environment +} + +const ( + cargoTomlFile = "Cargo.toml" + rocketTomlFile = "Rocket.toml" +) + +// RustTemplateConfig implements Nodejs config interface +type RustTemplateConfig struct { + Port int32 + AppName string + RocketToml string + RocketAddress string +} + +// CargoTomlConfig implements Cargo.toml config interface +type CargoTomlConfig struct { + Package PackageInfo +} + +// PackageInfo implements package properties +type PackageInfo struct { + Name string + Version string +} + +// RocketTomlConfig implements Rocket.toml config interface +type RocketTomlConfig struct { + Global GlobalParameters +} + +// GlobalParameters implements global properties in Rocket.toml +type GlobalParameters struct { + Address string + Port int32 +} + +// Init Initializes the transformer +func (t *RustDockerfileGenerator) Init(tc transformertypes.Transformer, env *environment.Environment) (err error) { + t.Config = tc + t.Env = env + return nil +} + +// GetConfig returns the transformer config +func (t *RustDockerfileGenerator) GetConfig() (transformertypes.Transformer, *environment.Environment) { + return t.Config, t.Env +} + +// DirectoryDetect runs detect in each sub directory +func (t *RustDockerfileGenerator) DirectoryDetect(dir string) (map[string][]transformertypes.Artifact, error) { + cargoPath := filepath.Join(dir, cargoTomlFile) + if _, err := os.Stat(cargoPath); err != nil { + return nil, nil + } + cargoTomlConfig := CargoTomlConfig{} + if _, err := toml.DecodeFile(cargoPath, &cargoTomlConfig); err != nil { + return nil, fmt.Errorf("failed to parse the cargo.toml file at path %s . Error: %q", cargoPath, err) + } + serviceName := cargoTomlConfig.Package.Name + normalizedServiceName := common.MakeStringK8sServiceNameCompliant(serviceName) + services := map[string][]transformertypes.Artifact{ + normalizedServiceName: {{ + Paths: map[transformertypes.PathType][]string{ + artifacts.ServiceDirPathType: {dir}, + }, + Configs: map[transformertypes.ConfigType]interface{}{ + artifacts.OriginalNameConfigType: artifacts.OriginalNameConfig{OriginalName: serviceName}, + }, + }}, + } + return services, nil +} + +// Transform transforms the artifacts +func (t *RustDockerfileGenerator) Transform(newArtifacts []transformertypes.Artifact, oldArtifacts []transformertypes.Artifact) ([]transformertypes.PathMapping, []transformertypes.Artifact, error) { + pathMappings := []transformertypes.PathMapping{} + artifactsCreated := []transformertypes.Artifact{} + for _, a := range newArtifacts { + if len(a.Paths[artifacts.ServiceDirPathType]) == 0 { + continue + } + relSrcPath, err := filepath.Rel(t.Env.GetEnvironmentSource(), a.Paths[artifacts.ServiceDirPathType][0]) + if err != nil { + logrus.Errorf("Unable to convert source path %s to be relative : %s", a.Paths[artifacts.ServiceDirPathType][0], err) + } + var sConfig artifacts.ServiceConfig + err = a.GetConfig(artifacts.ServiceConfigType, &sConfig) + if err != nil { + logrus.Errorf("Unable to load config for Transformer into %T : %s", sConfig, err) + continue + } + sImageName := artifacts.ImageName{} + err = a.GetConfig(artifacts.ImageNameConfigType, &sImageName) + if err != nil { + logrus.Debugf("Unable to load config for Transformer into %T : %s", sImageName, err) + } + //TODO: WASI + + //ir := irtypes.IR{} + //irPresent := true + //err = a.GetConfig(irtypes.IRConfigType, &ir) + //if err != nil { + // irPresent = false + // logrus.Debugf("unable to load config for Transformer into %T : %s", ir, err) + //} + //ports := ir.GetAllServicePorts() + ports := []int32{} + var rustConfig RustTemplateConfig + rustConfig.AppName = a.Name + rocketTomlFilePath := filepath.Join(a.Paths[artifacts.ServiceDirPathType][0], rocketTomlFile) + if _, err := os.Stat(rocketTomlFilePath); err == nil { + rustConfig.RocketToml = rocketTomlFile + var rocketTomlConfig RocketTomlConfig + if _, err := toml.DecodeFile(rocketTomlFilePath, &rocketTomlConfig); err == nil { + ports = append(ports, rocketTomlConfig.Global.Port) + rustConfig.RocketAddress = rocketTomlConfig.Global.Address + } + } + if len(ports) == 0 { + ports = append(ports, common.DefaultServicePort) + } + //rustConfig.Port = commonqa.GetPortForService(ports, `"`+a.Name+`"`) + if sImageName.ImageName == "" { + sImageName.ImageName = common.MakeStringContainerImageNameCompliant(sConfig.ServiceName) + } + pathMappings = append(pathMappings, transformertypes.PathMapping{ + Type: transformertypes.SourcePathMappingType, + DestPath: common.DefaultSourceDir, + }, transformertypes.PathMapping{ + Type: transformertypes.TemplatePathMappingType, + SrcPath: filepath.Join(t.Env.Context, t.Config.Spec.TemplatesDir), + DestPath: filepath.Join(common.DefaultSourceDir, relSrcPath), + TemplateConfig: rustConfig, + }) + paths := a.Paths + paths[artifacts.DockerfilePathType] = []string{filepath.Join(common.DefaultSourceDir, relSrcPath, common.DefaultDockerfileName)} + p := transformertypes.Artifact{ + Name: sImageName.ImageName, + Type: artifacts.DockerfileArtifactType, + Paths: paths, + Configs: map[transformertypes.ConfigType]interface{}{ + artifacts.ImageNameConfigType: sImageName, + }, + } + dfs := transformertypes.Artifact{ + Name: sConfig.ServiceName, + Type: artifacts.DockerfileForServiceArtifactType, + Paths: a.Paths, + Configs: map[transformertypes.ConfigType]interface{}{ + artifacts.ImageNameConfigType: sImageName, + artifacts.ServiceConfigType: sConfig, + }, + } + //TODO: WASI + + //if irPresent { + // dfs.Configs[irtypes.IRConfigType] = ir + //} + artifactsCreated = append(artifactsCreated, p, dfs) + } + return pathMappings, artifactsCreated, nil +} diff --git a/transformer/dockerfilegenerator/utils.go b/transformer/dockerfilegenerator/utils.go new file mode 100644 index 0000000..5ef71ec --- /dev/null +++ b/transformer/dockerfilegenerator/utils.go @@ -0,0 +1,46 @@ +/* + * Copyright IBM Corporation 2021 + * + * 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 dockerfilegenerator + +import ( + "github.com/hashicorp/go-version" + "github.com/sirupsen/logrus" +) + +// getNodeVersion returns the Node version to be used for the service +func getNodeVersion(versionConstraint, defaultNodejsVersion string, supportedVersions []string) string { + v1, err := version.NewVersion(versionConstraint) + if err == nil { + logrus.Debugf("the constraint is a Node version: %#v", v1) + return "v" + v1.String() + } + constraints, err := version.NewConstraint(versionConstraint) + if err != nil { + logrus.Errorf("failed to parse the Node version constraint string. Error: %q Actual: %s", err, versionConstraint) + return defaultNodejsVersion + } + logrus.Debugf("Node version constraints len = %d; constraints = %#v", constraints.Len(), constraints.String()) + for _, supportedVersion := range supportedVersions { + ver, _ := version.NewVersion(supportedVersion) + if constraints.Check(ver) { + logrus.Debugf("%#v satisfies constraints %#v\n", ver, constraints) + return supportedVersion + } + } + logrus.Infof("no supported Node version detected in package.json. Selecting default Node version- %s", defaultNodejsVersion) + return defaultNodejsVersion +} diff --git a/transformer/dockerfilegenerator/windows/consoleappdockerfilegenerator.go b/transformer/dockerfilegenerator/windows/consoleappdockerfilegenerator.go new file mode 100644 index 0000000..d687d7f --- /dev/null +++ b/transformer/dockerfilegenerator/windows/consoleappdockerfilegenerator.go @@ -0,0 +1,328 @@ +/* + * Copyright IBM Corporation 2021 + * + * 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 windows + +import ( + "encoding/xml" + "fmt" + "io" + "net" + "net/url" + "os" + "path/filepath" + "strconv" + "strings" + + "github.com/konveyor/move2kube-wasm/common" + "github.com/konveyor/move2kube-wasm/environment" + dotnetutils "github.com/konveyor/move2kube-wasm/transformer/dockerfilegenerator/dotnet" + //irtypes "github.com/konveyor/move2kube/types/ir" + //"github.com/konveyor/move2kube/types/qaengine/commonqa" + "github.com/konveyor/move2kube-wasm/types/source/dotnet" + transformertypes "github.com/konveyor/move2kube-wasm/types/transformer" + "github.com/konveyor/move2kube-wasm/types/transformer/artifacts" + "github.com/sirupsen/logrus" +) + +const ( + // AppConfigFilePathListType points to the go.mod file path + AppConfigFilePathListType transformertypes.PathType = "AppConfigFilePathList" + + // AppCfgFile is file name of App.Config file in dotnet projects + AppCfgFile = "App.config" +) + +// ConsoleTemplateConfig implements .Net Console config interface +type ConsoleTemplateConfig struct { + Ports []int32 + AppName string + BaseImageVersion string +} + +// WinConsoleAppDockerfileGenerator implements the Transformer interface +type WinConsoleAppDockerfileGenerator struct { + Config transformertypes.Transformer + Env *environment.Environment +} + +// Init Initializes the transformer +func (t *WinConsoleAppDockerfileGenerator) Init(tc transformertypes.Transformer, env *environment.Environment) (err error) { + t.Config = tc + t.Env = env + return nil +} + +// GetConfig returns the transformer config +func (t *WinConsoleAppDockerfileGenerator) GetConfig() (transformertypes.Transformer, *environment.Environment) { + return t.Config, t.Env +} + +// parseAppConfig parses the application config +func (t *WinConsoleAppDockerfileGenerator) parseAppConfigForPort(AppCfgFilePath string) ([]int32, error) { + appConfigFile, err := os.Open(AppCfgFilePath) + if err != nil { + return nil, fmt.Errorf("could not open the App.config file: %s", err) + } + + defer appConfigFile.Close() + + byteValue, _ := io.ReadAll(appConfigFile) + appCfg := dotnet.AppConfig{} + xml.Unmarshal(byteValue, &appCfg) + if err != nil { + return nil, fmt.Errorf("could not parse the App.config file: %s", err) + } + + ports := []int32{} + for _, addKey := range appCfg.AppCfgSettings.AddList { + parsedURL, err := url.ParseRequestURI(addKey.Value) + if err != nil { + logrus.Errorf("Could not parse URI: %s", err) + continue + } + + if parsedURL.Scheme == "" || parsedURL.Host == "" { + logrus.Warnf("Scheme or host is empty in URI") + continue + } + + _, port, err := net.SplitHostPort(parsedURL.Host) + if err != nil { + logrus.Errorf("Could not extract port from URI: %s", err) + continue + } + + portAsInt, err := strconv.ParseInt(port, 10, 32) + if err != nil { + logrus.Errorf("Could not process port from URI: %s", err) + continue + } + + ports = append(ports, int32(portAsInt)) + } + + if len(appCfg.Model.Services.ServiceList) == 0 { + return ports, nil + } + + for _, svc := range appCfg.Model.Services.ServiceList { + for _, addKey := range svc.Host.BaseAddresses.AddList { + parsedURL, err := url.ParseRequestURI(addKey.BaseAddress) + if err != nil { + logrus.Errorf("Could not parse URI: %s", err) + continue + } + + if parsedURL.Scheme == "" || parsedURL.Host == "" { + logrus.Warnf("Scheme or host is empty in URI") + continue + } + + _, port, err := net.SplitHostPort(parsedURL.Host) + if err != nil { + logrus.Errorf("Could not extract port from URI: %s", err) + continue + } + + portAsInt, err := strconv.ParseInt(port, 10, 32) + if err != nil { + logrus.Errorf("Could not process port from URI: %s", err) + continue + } + + ports = append(ports, int32(portAsInt)) + } + } + + return ports, nil +} + +// DirectoryDetect runs detect in each sub directory +func (t *WinConsoleAppDockerfileGenerator) DirectoryDetect(dir string) (map[string][]transformertypes.Artifact, error) { + slnPaths, err := common.GetFilesByExtInCurrDir(dir, []string{dotnet.VISUAL_STUDIO_SOLUTION_FILE_EXT}) + if err != nil { + return nil, fmt.Errorf("failed to list the dot net visual studio solution files in the directory %s . Error: %q", dir, err) + } + if len(slnPaths) == 0 { + return nil, nil + } + if len(slnPaths) > 1 { + logrus.Debugf("more than one visual studio solution file detected. Number of .sln files %d", len(slnPaths)) + } + slnPath := slnPaths[0] + appName := dotnetutils.GetParentProjectName(slnPath) + normalizedAppName := common.MakeStringK8sServiceNameCompliant(appName) + + relCSProjPaths, err := dotnetutils.GetCSProjPathsFromSlnFile(slnPath, false) + if err != nil { + return nil, fmt.Errorf("failed to get the .csproj paths from .sln file at path %s . Error: %q", slnPath, err) + } + + if len(relCSProjPaths) == 0 { + return nil, fmt.Errorf("no c sharp projects available found in the .sln at path %s", slnPath) + } + + appCfgFilePaths := []string{} + + found := false + for _, relCSProjPath := range relCSProjPaths { + csProjPath := filepath.Join(dir, strings.TrimSpace(relCSProjPath)) + csProjBytes, err := os.ReadFile(csProjPath) + if err != nil { + logrus.Errorf("failed to read the c sharp project file at path %s . Error: %q", csProjPath, err) + continue + } + configuration := dotnet.CSProj{} + if err := xml.Unmarshal(csProjBytes, &configuration); err != nil { + logrus.Errorf("failed to parse the xml c sharp project file at path %s . Error: %q", csProjPath, err) + continue + } + idx := common.FindIndex( + configuration.PropertyGroups, + func(x dotnet.PropertyGroup) bool { return x.TargetFrameworkVersion != "" }, + ) + if idx == -1 { + logrus.Debugf("failed to find the target framework in any of the property groups inside the c sharp project file at path %s", csProjPath) + continue + } + targetFrameworkVersion := configuration.PropertyGroups[idx].TargetFrameworkVersion + if !dotnet.Version4And3_5.MatchString(targetFrameworkVersion) { + logrus.Errorf("console dot net tranformer: the c sharp project file at path %s does not have a supported framework version. Actual version: %s", csProjPath, targetFrameworkVersion) + continue + } + isWebProj, err := isWeb(configuration) + if err != nil { + logrus.Errorf("failed to detect if it's a web/asp net project. Error: %q", err) + continue + } + if isWebProj { + continue + } + found = true + appCfgFilePath := filepath.Join(dir, filepath.Dir(relCSProjPath), AppCfgFile) + if _, err := os.Stat(appCfgFilePath); err != nil { + continue + } + appCfgFilePaths = append(appCfgFilePaths, appCfgFilePath) + } + + if !found { + return nil, nil + } + + services := map[string][]transformertypes.Artifact{ + normalizedAppName: {{ + Paths: map[transformertypes.PathType][]string{ + artifacts.ServiceDirPathType: {dir}, + AppConfigFilePathListType: appCfgFilePaths, + }, + Configs: map[transformertypes.ConfigType]interface{}{ + artifacts.OriginalNameConfigType: artifacts.OriginalNameConfig{OriginalName: appName}, + }, + }}, + } + return services, nil +} + +// Transform transforms the artifacts +func (t *WinConsoleAppDockerfileGenerator) Transform(newArtifacts []transformertypes.Artifact, alreadySeenArtifacts []transformertypes.Artifact) ([]transformertypes.PathMapping, []transformertypes.Artifact, error) { + pathMappings := []transformertypes.PathMapping{} + artifactsCreated := []transformertypes.Artifact{} + for _, newArtifact := range newArtifacts { + if len(newArtifact.Paths[artifacts.ServiceDirPathType]) == 0 { + continue + } + serviceDir := newArtifact.Paths[artifacts.ServiceDirPathType][0] + relServiceDir, err := filepath.Rel(t.Env.GetEnvironmentSource(), serviceDir) + if err != nil { + logrus.Errorf("Unable to convert source path %s to be relative. Error: %q", serviceDir, err) + continue + } + serviceConfig := artifacts.ServiceConfig{} + if err := newArtifact.GetConfig(artifacts.ServiceConfigType, &serviceConfig); err != nil { + logrus.Errorf("unable to load config for Transformer into %T : %s", serviceConfig, err) + continue + } + imageName := artifacts.ImageName{} + if err := newArtifact.GetConfig(artifacts.ImageNameConfigType, &imageName); err != nil { + logrus.Debugf("unable to load config for Transformer into %T : %s", imageName, err) + } + //TODO: WASI + //ir := irtypes.IR{} + //irPresent := true + //if err := newArtifact.GetConfig(irtypes.IRConfigType, &ir); err != nil { + // irPresent = false + // logrus.Debugf("unable to load config for Transformer into %T : %s", ir, err) + //} + detectedPorts := []int32{} + for _, appConfigFilePath := range newArtifact.Paths[AppConfigFilePathListType] { + portList, err := t.parseAppConfigForPort(appConfigFilePath) + if err != nil { + logrus.Errorf("%s", err) + continue + } + if portList != nil { + detectedPorts = append(detectedPorts, portList...) + } + } + //if len(detectedPorts) == 0 { + // detectedPorts = ir.GetAllServicePorts() + //} + //detectedPorts = commonqa.GetPortsForService(detectedPorts, `"`+newArtifact.Name+`"`) + var consoleConfig ConsoleTemplateConfig + consoleConfig.AppName = newArtifact.Name + consoleConfig.Ports = detectedPorts + consoleConfig.BaseImageVersion = dotnet.DefaultBaseImageVersion + + if imageName.ImageName == "" { + imageName.ImageName = common.MakeStringContainerImageNameCompliant(serviceConfig.ServiceName) + } + pathMappings = append(pathMappings, transformertypes.PathMapping{ + Type: transformertypes.SourcePathMappingType, + DestPath: common.DefaultSourceDir, + }, transformertypes.PathMapping{ + Type: transformertypes.TemplatePathMappingType, + SrcPath: filepath.Join(t.Env.Context, t.Config.Spec.TemplatesDir), + DestPath: filepath.Join(common.DefaultSourceDir, relServiceDir), + TemplateConfig: consoleConfig, + }) + paths := newArtifact.Paths + paths[artifacts.DockerfilePathType] = []string{filepath.Join(common.DefaultSourceDir, relServiceDir, common.DefaultDockerfileName)} + p := transformertypes.Artifact{ + Name: imageName.ImageName, + Type: artifacts.DockerfileArtifactType, + Paths: paths, + Configs: map[transformertypes.ConfigType]interface{}{ + artifacts.ImageNameConfigType: imageName, + }, + } + dfs := transformertypes.Artifact{ + Name: serviceConfig.ServiceName, + Type: artifacts.DockerfileForServiceArtifactType, + Paths: newArtifact.Paths, + Configs: map[transformertypes.ConfigType]interface{}{ + artifacts.ImageNameConfigType: imageName, + artifacts.ServiceConfigType: serviceConfig, + }, + } + //if irPresent { + // dfs.Configs[irtypes.IRConfigType] = ir + //} + artifactsCreated = append(artifactsCreated, p, dfs) + } + return pathMappings, artifactsCreated, nil +} diff --git a/transformer/dockerfilegenerator/windows/silverlightwebappdockerfilegenerator.go b/transformer/dockerfilegenerator/windows/silverlightwebappdockerfilegenerator.go new file mode 100644 index 0000000..dc6b227 --- /dev/null +++ b/transformer/dockerfilegenerator/windows/silverlightwebappdockerfilegenerator.go @@ -0,0 +1,224 @@ +/* + * Copyright IBM Corporation 2021 + * + * 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 windows + +import ( + "encoding/xml" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/konveyor/move2kube-wasm/common" + "github.com/konveyor/move2kube-wasm/environment" + dotnetutils "github.com/konveyor/move2kube-wasm/transformer/dockerfilegenerator/dotnet" + //irtypes "github.com/konveyor/move2kube-wasm/types/ir" + //"github.com/konveyor/move2kube-wasm/types/qaengine/commonqa" + "github.com/konveyor/move2kube-wasm/types/source/dotnet" + transformertypes "github.com/konveyor/move2kube-wasm/types/transformer" + "github.com/konveyor/move2kube-wasm/types/transformer/artifacts" + "github.com/sirupsen/logrus" +) + +// SilverLightTemplateConfig implements SilverLight config interface +type SilverLightTemplateConfig struct { + Ports []int32 + AppName string +} + +// WinSilverLightWebAppDockerfileGenerator implements the Transformer interface +type WinSilverLightWebAppDockerfileGenerator struct { + Config transformertypes.Transformer + Env *environment.Environment +} + +// Init Initializes the transformer +func (t *WinSilverLightWebAppDockerfileGenerator) Init(tc transformertypes.Transformer, env *environment.Environment) (err error) { + t.Config = tc + t.Env = env + return nil +} + +// GetConfig returns the transformer config +func (t *WinSilverLightWebAppDockerfileGenerator) GetConfig() (transformertypes.Transformer, *environment.Environment) { + return t.Config, t.Env +} + +// DirectoryDetect runs detect in each sub directory +func (t *WinSilverLightWebAppDockerfileGenerator) DirectoryDetect(dir string) (map[string][]transformertypes.Artifact, error) { + slnPaths, err := common.GetFilesByExtInCurrDir(dir, []string{dotnet.VISUAL_STUDIO_SOLUTION_FILE_EXT}) + if err != nil { + return nil, fmt.Errorf("failed to list the dot net visual studio solution files in the directory %s . Error: %q", dir, err) + } + if len(slnPaths) == 0 { + return nil, nil + } + if len(slnPaths) > 1 { + logrus.Debugf("more than one visual studio solution file detected. Number of .sln files %d", len(slnPaths)) + } + slnPath := slnPaths[0] + appName := dotnetutils.GetParentProjectName(slnPath) + normalizedAppName := common.MakeStringK8sServiceNameCompliant(appName) + + relCSProjPaths, err := dotnetutils.GetCSProjPathsFromSlnFile(slnPath, false) + if err != nil { + return nil, fmt.Errorf("failed to get the .csproj paths from .sln file at path %s . Error: %q", slnPath, err) + } + + if len(relCSProjPaths) == 0 { + return nil, fmt.Errorf("no c sharp projects available found in the.sln path %s", slnPath) + } + + found := false + for _, relCSProjPath := range relCSProjPaths { + csProjPath := filepath.Join(dir, strings.TrimSpace(relCSProjPath)) + csProjBytes, err := os.ReadFile(csProjPath) + if err != nil { + logrus.Errorf("failed to read the c sharp project file at path %s . Error: %q", csProjPath, err) + continue + } + + configuration := dotnet.CSProj{} + if err := xml.Unmarshal(csProjBytes, &configuration); err != nil { + logrus.Errorf("failed to parse the project file at path %s . Error: %q", csProjPath, err) + continue + } + + idx := common.FindIndex(configuration.PropertyGroups, func(x dotnet.PropertyGroup) bool { return x.TargetFrameworkVersion != "" }) + if idx == -1 { + logrus.Debugf("failed to find the target framework in any of the property groups inside the c sharp project file at path %s", csProjPath) + continue + } + targetFrameworkVersion := configuration.PropertyGroups[idx].TargetFrameworkVersion + if !dotnet.Version4And3_5.MatchString(targetFrameworkVersion) { + logrus.Errorf("silverlight dot net tranformer: the c sharp project file at path %s does not have a supported framework version. Actual version: %s", csProjPath, targetFrameworkVersion) + continue + } + + isWebProj, err := isWeb(configuration) + if err != nil { + logrus.Errorf("failed to detect if it's a web/asp net project. Error: %q", err) + continue + } + if !isWebProj { + continue + } + + isSLProj, err := isSilverlight(configuration) + if err != nil { + logrus.Errorf("failed to detect if it's a silverlight project. Error: %q", err) + continue + } + if !isSLProj { + continue + } + + found = true + } + + if !found { + return nil, nil + } + + services := map[string][]transformertypes.Artifact{ + normalizedAppName: {{ + Paths: map[transformertypes.PathType][]string{ + artifacts.ServiceDirPathType: {dir}, + }, + Configs: map[transformertypes.ConfigType]interface{}{ + artifacts.OriginalNameConfigType: artifacts.OriginalNameConfig{OriginalName: appName}, + }, + }}, + } + return services, nil +} + +// Transform transforms the artifacts +func (t *WinSilverLightWebAppDockerfileGenerator) Transform(newArtifacts []transformertypes.Artifact, alreadySeenArtifacts []transformertypes.Artifact) ([]transformertypes.PathMapping, []transformertypes.Artifact, error) { + pathMappings := []transformertypes.PathMapping{} + artifactsCreated := []transformertypes.Artifact{} + for _, a := range newArtifacts { + relSrcPath, err := filepath.Rel(t.Env.GetEnvironmentSource(), a.Paths[artifacts.ServiceDirPathType][0]) + if err != nil { + logrus.Errorf("Unable to convert source path %s to be relative : %s", a.Paths[artifacts.ServiceDirPathType][0], err) + } + var sConfig artifacts.ServiceConfig + err = a.GetConfig(artifacts.ServiceConfigType, &sConfig) + if err != nil { + logrus.Errorf("unable to load config for Transformer into %T : %s", sConfig, err) + continue + } + sImageName := artifacts.ImageName{} + err = a.GetConfig(artifacts.ImageNameConfigType, &sImageName) + if err != nil { + logrus.Debugf("unable to load config for Transformer into %T : %s", sImageName, err) + } + //TODO: WASI + //ir := irtypes.IR{} + //irPresent := true + //err = a.GetConfig(irtypes.IRConfigType, &ir) + //if err != nil { + // irPresent = false + // logrus.Debugf("unable to load config for Transformer into %T : %s", ir, err) + //} + //detectedPorts := ir.GetAllServicePorts() + //if len(detectedPorts) == 0 { + // detectedPorts = append(detectedPorts, common.DefaultServicePort) + //} + //detectedPorts = commonqa.GetPortsForService(detectedPorts, `"`+a.Name+`"`) + var silverLightConfig SilverLightTemplateConfig + silverLightConfig.AppName = a.Name + //silverLightConfig.Ports = detectedPorts + if sImageName.ImageName == "" { + sImageName.ImageName = common.MakeStringContainerImageNameCompliant(sConfig.ServiceName) + } + pathMappings = append(pathMappings, transformertypes.PathMapping{ + Type: transformertypes.SourcePathMappingType, + DestPath: common.DefaultSourceDir, + }, transformertypes.PathMapping{ + Type: transformertypes.TemplatePathMappingType, + SrcPath: filepath.Join(t.Env.Context, t.Config.Spec.TemplatesDir), + DestPath: filepath.Join(common.DefaultSourceDir, relSrcPath), + TemplateConfig: silverLightConfig, + }) + paths := a.Paths + paths[artifacts.DockerfilePathType] = []string{filepath.Join(common.DefaultSourceDir, relSrcPath, common.DefaultDockerfileName)} + p := transformertypes.Artifact{ + Name: sImageName.ImageName, + Type: artifacts.DockerfileArtifactType, + Paths: paths, + Configs: map[transformertypes.ConfigType]interface{}{ + artifacts.ImageNameConfigType: sImageName, + }, + } + dfs := transformertypes.Artifact{ + Name: sConfig.ServiceName, + Type: artifacts.DockerfileForServiceArtifactType, + Paths: a.Paths, + Configs: map[transformertypes.ConfigType]interface{}{ + artifacts.ImageNameConfigType: sImageName, + artifacts.ServiceConfigType: sConfig, + }, + } + //TODO: WASI + //if irPresent { + // dfs.Configs[irtypes.IRConfigType] = ir + //} + artifactsCreated = append(artifactsCreated, p, dfs) + } + return pathMappings, artifactsCreated, nil +} diff --git a/transformer/dockerfilegenerator/windows/types.go b/transformer/dockerfilegenerator/windows/types.go new file mode 100644 index 0000000..1890711 --- /dev/null +++ b/transformer/dockerfilegenerator/windows/types.go @@ -0,0 +1,41 @@ +/* + * Copyright IBM Corporation 2022 + * + * 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 windows + +import ( + "github.com/konveyor/move2kube-wasm/types" +) + +const ( + versionMappingFilePath = "mappings/dotnetwindowsversionmapping.yaml" + // DotNetWindowsVersionMappingKind defines the K8s kind for the version mapping file + DotNetWindowsVersionMappingKind types.Kind = "DotNetWindowsVersionMapping" +) + +// DotNetWindowsVersionMapping stores the dot net version mapping +type DotNetWindowsVersionMapping struct { + types.TypeMeta `yaml:",inline"` + types.ObjectMeta `yaml:"metadata,omitempty"` + Spec DotNetWindowsVersionMappingSpec `yaml:"spec,omitempty"` +} + +// DotNetWindowsVersionMappingSpec stores the dot net version mapping spec +type DotNetWindowsVersionMappingSpec struct { + // imageTagToSupportedVersions is a mapping from image tag to dot net framework versions that image supports. + // Version compatibility table taken from https://hub.docker.com/_/microsoft-dotnet-framework-aspnet + ImageTagToSupportedVersions map[string][]string `yaml:"imageTagToSupportedVersions"` +} diff --git a/transformer/dockerfilegenerator/windows/utils.go b/transformer/dockerfilegenerator/windows/utils.go new file mode 100644 index 0000000..c199e3f --- /dev/null +++ b/transformer/dockerfilegenerator/windows/utils.go @@ -0,0 +1,59 @@ +/* + * Copyright IBM Corporation 2021 + * + * 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 windows + +import ( + "fmt" + + "github.com/konveyor/move2kube-wasm/types/source/dotnet" +) + +// isSilverlight checks if the app is silverlight by looking for silverlight regex patterns +func isSilverlight(configuration dotnet.CSProj) (bool, error) { + if configuration.ItemGroups == nil || len(configuration.ItemGroups) == 0 { + return false, fmt.Errorf("no item groups in project file to parse") + } + for _, ig := range configuration.ItemGroups { + if len(ig.Contents) == 0 { + continue + } + for _, r := range ig.Contents { + if dotnet.WebSLLib.MatchString(r.Include) { + return true, nil + } + } + } + return false, nil +} + +// isWeb checks if the given app is a web app +func isWeb(configuration dotnet.CSProj) (bool, error) { + if configuration.ItemGroups == nil || len(configuration.ItemGroups) == 0 { + return false, fmt.Errorf("no item groups in project file to parse") + } + for _, ig := range configuration.ItemGroups { + if len(ig.References) == 0 { + continue + } + for _, r := range ig.References { + if dotnet.WebLib.MatchString(r.Include) { + return true, nil + } + } + } + return false, nil +} diff --git a/transformer/dockerfilegenerator/windows/webappdockerfilegenerator.go b/transformer/dockerfilegenerator/windows/webappdockerfilegenerator.go new file mode 100644 index 0000000..d4271ab --- /dev/null +++ b/transformer/dockerfilegenerator/windows/webappdockerfilegenerator.go @@ -0,0 +1,450 @@ +/* + * Copyright IBM Corporation 2021 + * + * 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 windows + +import ( + "fmt" + "path/filepath" + + "github.com/konveyor/move2kube-wasm/common" + "github.com/konveyor/move2kube-wasm/environment" + //"github.com/konveyor/move2kube-wasm/qaengine" + dotnetutils "github.com/konveyor/move2kube-wasm/transformer/dockerfilegenerator/dotnet" + //irtypes "github.com/konveyor/move2kube-wasm/types/ir" + //"github.com/konveyor/move2kube-wasm/types/qaengine/commonqa" + "github.com/konveyor/move2kube-wasm/types/source/dotnet" + transformertypes "github.com/konveyor/move2kube-wasm/types/transformer" + "github.com/konveyor/move2kube-wasm/types/transformer/artifacts" + "github.com/sirupsen/logrus" + "golang.org/x/mod/semver" +) + +const ( + buildStageC = "dotnetwebbuildstage" + defaultBuildOutputDir = "bin" + defaultRunStageImageTag = "4.8" +) + +func (t *WinWebAppDockerfileGenerator) getImageTagFromVersion(version string) string { + for imageTag, versions := range t.imageTagToSupportedVersions { + if common.FindIndex(versions, func(v string) bool { return v == version || "v"+v == version }) != -1 { + return imageTag + } + } + return defaultRunStageImageTag +} + +// WebTemplateConfig contains the data to fill the Dockerfile template +type WebTemplateConfig struct { + Ports []int32 + IncludeBuildStage bool + BuildStageImageTag string + BuildContainerName string + IncludeRunStage bool + RunStageImageTag string + CopyFrom string +} + +// WinWebAppDockerfileGenerator implements the Transformer interface +type WinWebAppDockerfileGenerator struct { + Config transformertypes.Transformer + Env *environment.Environment + imageTagToSupportedVersions map[string][]string +} + +// Init Initializes the transformer +func (t *WinWebAppDockerfileGenerator) Init(tc transformertypes.Transformer, env *environment.Environment) error { + t.Config = tc + t.Env = env + mappingFile := DotNetWindowsVersionMapping{} + mappingFilePath := filepath.Join(t.Env.GetEnvironmentContext(), versionMappingFilePath) + if err := common.ReadMove2KubeYaml(mappingFilePath, &mappingFile); err != nil { + return fmt.Errorf("failed to load the dot net windows version mapping file at path %s . Error: %q", mappingFilePath, err) + } + if len(mappingFile.Spec.ImageTagToSupportedVersions) == 0 { + return fmt.Errorf("the mapping file at path %s is invalid", mappingFilePath) + } + t.imageTagToSupportedVersions = mappingFile.Spec.ImageTagToSupportedVersions + return nil +} + +// GetConfig returns the transformer config +func (t *WinWebAppDockerfileGenerator) GetConfig() (transformertypes.Transformer, *environment.Environment) { + return t.Config, t.Env +} + +// DirectoryDetect runs detect in each sub directory +func (t *WinWebAppDockerfileGenerator) DirectoryDetect(dir string) (map[string][]transformertypes.Artifact, error) { + slnPaths, err := common.GetFilesByExtInCurrDir(dir, []string{dotnet.VISUAL_STUDIO_SOLUTION_FILE_EXT}) + if err != nil { + return nil, fmt.Errorf("failed to list the dot net visual studio solution files in the directory %s . Error: %q", dir, err) + } + if len(slnPaths) == 0 { + csProjPaths, err := common.GetFilesByExtInCurrDir(dir, []string{dotnetutils.CSPROJ_FILE_EXT}) + if err != nil { + return nil, fmt.Errorf("failed to list the dot net c sharp project files in the directory %s . Error: %q", dir, err) + } + if len(csProjPaths) == 0 { + return nil, nil + } + childProjects := []artifacts.DotNetChildProject{} + for _, csProjPath := range csProjPaths { + configuration, err := dotnetutils.ParseCSProj(csProjPath) + if err != nil { + logrus.Errorf("failed to parse the c sharp project file at path %s . Error: %q", csProjPath, err) + continue + } + idx := common.FindIndex(configuration.PropertyGroups, func(x dotnet.PropertyGroup) bool { return x.TargetFrameworkVersion != "" }) + if idx == -1 { + logrus.Debugf("failed to find the target framework in any of the property groups inside the c sharp project file at path %s", csProjPath) + continue + } + targetFrameworkVersion := configuration.PropertyGroups[idx].TargetFrameworkVersion + if !dotnet.Version4And3_5.MatchString(targetFrameworkVersion) { + logrus.Errorf("the c sharp project file at path %s does not have a supported framework version. Actual version: %s", csProjPath, targetFrameworkVersion) + continue + } + childProjectName := dotnetutils.GetChildProjectName(csProjPath) + normalizedChildProjectName := common.MakeStringK8sServiceNameCompliant(childProjectName) + childProjects = append(childProjects, artifacts.DotNetChildProject{ + Name: normalizedChildProjectName, + OriginalName: childProjectName, + RelCSProjPath: filepath.Base(csProjPath), + TargetFramework: targetFrameworkVersion, + }) + } + appName := dotnetutils.GetChildProjectName(csProjPaths[0]) + normalizedAppName := common.MakeStringK8sServiceNameCompliant(appName) + namedServices := map[string][]transformertypes.Artifact{ + normalizedAppName: {{ + Paths: map[transformertypes.PathType][]string{ + artifacts.ServiceRootDirPathType: {dir}, + artifacts.ServiceDirPathType: {dir}, + dotnetutils.DotNetCoreCsprojFilesPathType: csProjPaths, + }, + Configs: map[transformertypes.ConfigType]interface{}{ + artifacts.DotNetConfigType: artifacts.DotNetConfig{ + DotNetAppName: appName, + IsSolutionFilePresent: false, + ChildProjects: childProjects, + }, + }, + }}, + } + return namedServices, nil + } + if len(slnPaths) > 1 { + logrus.Debugf("more than one visual studio solution file detected. Number of .sln files %d", len(slnPaths)) + } + slnPath := slnPaths[0] + appName := dotnetutils.GetParentProjectName(slnPath) + normalizedAppName := common.MakeStringK8sServiceNameCompliant(appName) + relCSProjPaths, err := dotnetutils.GetCSProjPathsFromSlnFile(slnPath, false) + if err != nil { + return nil, fmt.Errorf("failed to parse the vs solution file at path %s . Error: %q", slnPath, err) + } + if len(relCSProjPaths) == 0 { + return nil, fmt.Errorf("no child projects found in the solution file at the path: %s", slnPath) + } + childProjects := []artifacts.DotNetChildProject{} + csProjPaths := []string{} + foundNonSilverLightWebProject := false + for _, relCSProjPath := range relCSProjPaths { + csProjPath := filepath.Join(dir, relCSProjPath) + configuration, err := dotnetutils.ParseCSProj(csProjPath) + if err != nil { + logrus.Errorf("failed to parse the c sharp project file at path %s . Error: %q", csProjPath, err) + continue + } + idx := common.FindIndex(configuration.PropertyGroups, func(x dotnet.PropertyGroup) bool { return x.TargetFrameworkVersion != "" }) + if idx == -1 { + logrus.Debugf("failed to find the target framework in any of the property groups inside the c sharp project file at path %s", csProjPath) + continue + } + targetFrameworkVersion := configuration.PropertyGroups[idx].TargetFrameworkVersion + if !dotnet.Version4And3_5.MatchString(targetFrameworkVersion) { + logrus.Errorf("webapp dot net tranformer: the c sharp project file at path %s does not have a supported framework version. Actual version: %s", csProjPath, targetFrameworkVersion) + continue + } + isWebProj, err := isWeb(configuration) + if err != nil { + logrus.Errorf("failed to determine if the c sharp project file at the path %s is a web project. Error: %q", csProjPath, err) + continue + } + if !isWebProj { + logrus.Debugf("the c sharp project file at path %s is not a web project", csProjPath) + continue + } + isSLProj, err := isSilverlight(configuration) + if err != nil { + logrus.Errorf("failed to determine if the c sharp project file at the path %s is a SilverLight project. Error: %q", csProjPath, err) + continue + } + if isSLProj { + logrus.Debugf("the c sharp project file at path %s is a SilverLight project", csProjPath) + continue + } + foundNonSilverLightWebProject = true + childProjectName := dotnetutils.GetChildProjectName(csProjPath) + normalizedChildProjectName := common.MakeStringK8sServiceNameCompliant(childProjectName) + csProjPaths = append(csProjPaths, csProjPath) + childProjects = append(childProjects, artifacts.DotNetChildProject{ + Name: normalizedChildProjectName, + OriginalName: childProjectName, + RelCSProjPath: relCSProjPath, + }) + } + if !foundNonSilverLightWebProject { + return nil, nil + } + namedServices := map[string][]transformertypes.Artifact{ + normalizedAppName: {{ + Name: normalizedAppName, + Type: artifacts.ServiceArtifactType, + Paths: map[transformertypes.PathType][]string{ + artifacts.ServiceRootDirPathType: {dir}, + artifacts.ServiceDirPathType: {dir}, + dotnetutils.DotNetCoreSolutionFilePathType: {slnPath}, + dotnetutils.DotNetCoreCsprojFilesPathType: csProjPaths, + }, + Configs: map[transformertypes.ConfigType]interface{}{ + artifacts.DotNetConfigType: artifacts.DotNetConfig{ + IsDotNetCore: false, + DotNetAppName: appName, + IsSolutionFilePresent: true, + ChildProjects: childProjects, + }, + }, + }}, + } + return namedServices, nil +} + +// Transform transforms the artifacts +func (t *WinWebAppDockerfileGenerator) Transform(newArtifacts []transformertypes.Artifact, alreadySeenArtifacts []transformertypes.Artifact) ([]transformertypes.PathMapping, []transformertypes.Artifact, error) { + pathMappings := []transformertypes.PathMapping{} + artifactsCreated := []transformertypes.Artifact{} + for _, newArtifact := range newArtifacts { + dotNetConfig := artifacts.DotNetConfig{} + if err := newArtifact.GetConfig(artifacts.DotNetConfigType, &dotNetConfig); err != nil || dotNetConfig.IsDotNetCore { + continue + } + if len(newArtifact.Paths[artifacts.ServiceDirPathType]) == 0 || len(newArtifact.Paths[dotnetutils.DotNetCoreCsprojFilesPathType]) == 0 { + logrus.Errorf("the service directory is missing from the dot net artifact %+v", newArtifact) + continue + } + selectedBuildOption, err := dotnetutils.AskUserForDockerfileType(newArtifact.Name) + if err != nil { + logrus.Errorf("failed to ask the user what type of dockerfile they prefer. Error: %q", err) + continue + } + logrus.Debugf("user chose to generate Dockefiles that have '%s'", selectedBuildOption) + + // ask the user which child projects should be run in the K8s cluster + + selectedChildProjectNames := []string{} + for _, childProject := range dotNetConfig.ChildProjects { + selectedChildProjectNames = append(selectedChildProjectNames, childProject.Name) + } + if len(selectedChildProjectNames) > 1 { + //TODO: WASI + //quesKey := fmt.Sprintf(common.ConfigServicesDotNetChildProjectsNamesKey, `"`+newArtifact.Name+`"`) + //desc := fmt.Sprintf("For the multi-project Dot Net app '%s', please select all the child projects that should be run as services in the cluster:", newArtifact.Name) + //hints := []string{"deselect any child project that should not be run (example: libraries)"} + //selectedChildProjectNames = qaengine.FetchMultiSelectAnswer(quesKey, desc, hints, selectedChildProjectNames, selectedChildProjectNames, nil) + if len(selectedChildProjectNames) == 0 { + return pathMappings, artifactsCreated, fmt.Errorf("user deselected all the child projects of the dot net multi-project app '%s'", newArtifact.Name) + } + } + + serviceDir := newArtifact.Paths[artifacts.ServiceRootDirPathType][0] + relServiceDir, err := filepath.Rel(t.Env.GetEnvironmentSource(), serviceDir) + if err != nil { + logrus.Errorf("failed to make the service directory path %s relative to the source directory %s . Error: %q", serviceDir, t.Env.GetEnvironmentSource(), err) + continue + } + //TODO: WASI + //ir := irtypes.IR{} + //irPresent := true + //if err := newArtifact.GetConfig(irtypes.IRConfigType, &ir); err != nil { + // irPresent = false + // logrus.Debugf("failed to load the IR config from the dot net artifact. Error: %q Artifact: %+v", err, newArtifact) + //} + // + //detectedPorts := ir.GetAllServicePorts() + //if len(detectedPorts) == 0 { + // detectedPorts = append(detectedPorts, common.DefaultServicePort) + //} + + // copy over the source dir to hold the dockerfiles we genrate + + pathMappings = append(pathMappings, transformertypes.PathMapping{ + Type: transformertypes.SourcePathMappingType, + DestPath: common.DefaultSourceDir, + }) + + // build is always done at the top level using the .sln file regardless of the build option selected + + imageToCopyFrom := common.MakeStringContainerImageNameCompliant(newArtifact.Name + "-" + buildStageC) + if selectedBuildOption == dotnetutils.NO_BUILD_STAGE { + imageToCopyFrom = "" // files will be copied from the local file system instead of a builder image + } + + highestFrameworkVersion := "" + currPathMappings := []transformertypes.PathMapping{} + currArtifactsCreated := []transformertypes.Artifact{} + + for _, childProject := range dotNetConfig.ChildProjects { + + // only look at the child modules the user selected + + if !common.IsPresent(selectedChildProjectNames, childProject.Name) { + continue + } + + // parse the .csproj file to get the output path + csProjPath := filepath.Join(serviceDir, childProject.RelCSProjPath) + configuration, err := dotnetutils.ParseCSProj(csProjPath) + if err != nil { + logrus.Errorf("failed to parse the c sharp project file at path %s . Error: %q", csProjPath, err) + continue + } + + // have the user select the ports to use for the child project + //TODO: WASI + //selectedPorts := commonqa.GetPortsForService(detectedPorts, common.JoinQASubKeys(`"`+newArtifact.Name+`"`, "childProjects", `"`+childProject.Name+`"`)) + selectedPorts := []int32{} + // data to fill the Dockerfile template + + relCSProjDir := filepath.Dir(childProject.RelCSProjPath) + buildOutputDir := defaultBuildOutputDir + if idx := common.FindIndex(configuration.PropertyGroups, func(x dotnet.PropertyGroup) bool { return x.OutputPath != "" }); idx != -1 { + buildOutputDir = filepath.Clean(common.GetUnixPath(configuration.PropertyGroups[idx].OutputPath)) + } + copyFrom := filepath.Join("/app", relCSProjDir, buildOutputDir) + "/" + if selectedBuildOption == dotnetutils.NO_BUILD_STAGE { + copyFrom = buildOutputDir + "/" // files will be copied from the local file system instead of a builder image + } + + targetFrameworkVersion := "" + if idx := common.FindIndex(configuration.PropertyGroups, func(x dotnet.PropertyGroup) bool { return x.TargetFrameworkVersion != "" }); idx != -1 { + targetFrameworkVersion = configuration.PropertyGroups[idx].TargetFrameworkVersion + } + + // keep track of the highest version among child projects + if targetFrameworkVersion != "" { + if highestFrameworkVersion == "" || (semver.IsValid(targetFrameworkVersion) && semver.Compare(highestFrameworkVersion, targetFrameworkVersion) == -1) { + highestFrameworkVersion = targetFrameworkVersion + logrus.Infof("Found a higher dot net framework version '%s'", highestFrameworkVersion) + } + } + + webConfig := WebTemplateConfig{ + Ports: selectedPorts, + BuildStageImageTag: t.getImageTagFromVersion(targetFrameworkVersion), + IncludeBuildStage: selectedBuildOption == dotnetutils.BUILD_IN_EVERY_IMAGE, + IncludeRunStage: true, + BuildContainerName: imageToCopyFrom, + CopyFrom: common.GetUnixPath(copyFrom), + RunStageImageTag: t.getImageTagFromVersion(targetFrameworkVersion), + } + + // path mapping to generate the Dockerfile for the child project + + dockerfilePath := filepath.Join(common.DefaultSourceDir, relServiceDir, relCSProjDir, common.DefaultDockerfileName) + currPathMappings = append(currPathMappings, transformertypes.PathMapping{ + Type: transformertypes.TemplatePathMappingType, + SrcPath: common.DefaultDockerfileName, + DestPath: dockerfilePath, + TemplateConfig: webConfig, + }) + + // artifacts to inform other transformers of the Dockerfile we generated + + paths := map[transformertypes.PathType][]string{artifacts.DockerfilePathType: {dockerfilePath}} + serviceName := artifacts.ServiceConfig{ServiceName: childProject.Name} + imageName := artifacts.ImageName{ImageName: common.MakeStringContainerImageNameCompliant(childProject.Name)} + dockerfileArtifact := transformertypes.Artifact{ + Name: imageName.ImageName, + Type: artifacts.DockerfileArtifactType, + Paths: paths, + Configs: map[transformertypes.ConfigType]interface{}{ + artifacts.ServiceConfigType: serviceName, + artifacts.ImageNameConfigType: imageName, + }, + } + dockerfileServiceArtifact := transformertypes.Artifact{ + Name: imageName.ImageName, + Type: artifacts.DockerfileForServiceArtifactType, + Paths: paths, + Configs: map[transformertypes.ConfigType]interface{}{ + artifacts.ServiceConfigType: serviceName, + artifacts.ImageNameConfigType: imageName, + }, + } + //TODO: WASI + //if irPresent { + // dockerfileServiceArtifact.Configs[irtypes.IRConfigType] = ir + //} + currArtifactsCreated = append(currArtifactsCreated, dockerfileArtifact, dockerfileServiceArtifact) + } + + // generate the base image Dockerfile + + if selectedBuildOption == dotnetutils.BUILD_IN_BASE_IMAGE { + if highestFrameworkVersion == "" { + highestFrameworkVersion = dotnet.DefaultBaseImageVersion + } + logrus.Infof("Using the highest dot net framework version found '%s' for the build stage.", highestFrameworkVersion) + webConfig := WebTemplateConfig{ + BuildStageImageTag: t.getImageTagFromVersion(highestFrameworkVersion), + IncludeBuildStage: true, + IncludeRunStage: false, + BuildContainerName: imageToCopyFrom, + } + + // path mapping to generate the Dockerfile for the child project + + dockerfilePath := filepath.Join(common.DefaultSourceDir, relServiceDir, common.DefaultDockerfileName+"."+buildStageC) + currPathMappings = append([]transformertypes.PathMapping{{ + Type: transformertypes.TemplatePathMappingType, + SrcPath: common.DefaultDockerfileName, + DestPath: dockerfilePath, + TemplateConfig: webConfig, + }}, currPathMappings...) + + // artifacts to inform other transformers of the Dockerfile we generated + + paths := map[transformertypes.PathType][]string{artifacts.DockerfilePathType: {dockerfilePath}} + serviceName := artifacts.ServiceConfig{ServiceName: newArtifact.Name} + imageName := artifacts.ImageName{ImageName: imageToCopyFrom} + dockerfileArtifact := transformertypes.Artifact{ + Name: imageName.ImageName, + Type: artifacts.DockerfileArtifactType, + Paths: paths, + Configs: map[transformertypes.ConfigType]interface{}{ + artifacts.ServiceConfigType: serviceName, + artifacts.ImageNameConfigType: imageName, + }, + } + currArtifactsCreated = append([]transformertypes.Artifact{dockerfileArtifact}, currArtifactsCreated...) + } + pathMappings = append(pathMappings, currPathMappings...) + artifactsCreated = append(artifactsCreated, currArtifactsCreated...) + } + return pathMappings, artifactsCreated, nil +}