diff --git a/internal/app/api/wwapid/wwapid.go b/internal/app/api/wwapid/wwapid.go index 5042b8c7..1d5419eb 100644 --- a/internal/app/api/wwapid/wwapid.go +++ b/internal/app/api/wwapid/wwapid.go @@ -15,8 +15,8 @@ import ( "github.com/hpcng/warewulf/internal/pkg/api/container" apinode "github.com/hpcng/warewulf/internal/pkg/api/node" "github.com/hpcng/warewulf/internal/pkg/api/routes/wwapiv1" - "github.com/hpcng/warewulf/internal/pkg/version" warewulfconf "github.com/hpcng/warewulf/internal/pkg/config" + "github.com/hpcng/warewulf/internal/pkg/version" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials" @@ -157,6 +157,25 @@ func (s *apiServer) ContainerBuild(ctx context.Context, request *wwapiv1.Contain return } +// ContainerCopy duplicates a container. +func (s *apiServer) ContainerCopy(ctx context.Context, request *wwapiv1.ContainerCopyParameter) (response *wwapiv1.ContainerListResponse, err error) { + + // Parameter checks. + if request == nil { + return response, status.Errorf(codes.InvalidArgument, "nil request") + } + + if request.ContainerSource == "" { + return response, status.Errorf(codes.InvalidArgument, "nil request.ContainerSource") + } + + if request.ContainerDestination == "" { + return response, status.Errorf(codes.InvalidArgument, "nil request.ContainerDest") + } + + return +} + // ContainerDelete deletes one or more containers from Warewulf. func (s *apiServer) ContainerDelete(ctx context.Context, request *wwapiv1.ContainerDeleteParameter) (response *emptypb.Empty, err error) { diff --git a/internal/app/wwctl/container/copy/main.go b/internal/app/wwctl/container/copy/main.go new file mode 100644 index 00000000..f3499f78 --- /dev/null +++ b/internal/app/wwctl/container/copy/main.go @@ -0,0 +1,48 @@ +package copy + +import ( + "fmt" + + "github.com/hpcng/warewulf/internal/pkg/api/routes/wwapiv1" + "github.com/hpcng/warewulf/internal/pkg/container" + "github.com/hpcng/warewulf/internal/pkg/wwlog" + "github.com/spf13/cobra" +) + +func CobraRunE(cmd *cobra.Command, args []string) (err error) { + + if len(args) > 2 { + wwlog.Warn("cp only requires 2 arguments but you provided %d arguments. Hence, they will be ignored.", len(args)) + } + + cdp := &wwapiv1.ContainerCopyParameter{ + ContainerSource: args[0], + ContainerDestination: args[1], + } + + if !container.DoesSourceExist(cdp.ContainerSource) { + wwlog.Error("Container's source doesn't exists: %s", cdp.ContainerSource) + return + } + + if !container.ValidName(cdp.ContainerDestination) { + wwlog.Error("Container name contains illegal characters : %s", cdp.ContainerDestination) + return + } + + if container.DoesSourceExist(cdp.ContainerDestination) { + wwlog.Error("An other container with name: %s already exists in sources.", cdp.ContainerDestination) + return + } + + err = container.Duplicate(cdp.ContainerSource, cdp.ContainerDestination) + if err != nil { + err = fmt.Errorf("could not duplicate image: %s", err.Error()) + wwlog.Error(err.Error()) + return + } + + wwlog.Info("Container %s successfully duplicated as %s", cdp.ContainerSource, cdp.ContainerDestination) + return + +} diff --git a/internal/app/wwctl/container/copy/root.go b/internal/app/wwctl/container/copy/root.go new file mode 100644 index 00000000..5490adce --- /dev/null +++ b/internal/app/wwctl/container/copy/root.go @@ -0,0 +1,33 @@ +package copy + +import ( + "github.com/hpcng/warewulf/internal/pkg/container" + "github.com/spf13/cobra" +) + +var ( + baseCmd = &cobra.Command{ + DisableFlagsInUseLine: true, + Use: "cp CONTAINER NEW_NAME", + Short: "Copy an existing container", + Long: "This command will duplicate an imported container image.", + RunE: CobraRunE, + Args: cobra.MinimumNArgs(2), + ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if len(args) != 0 { + return nil, cobra.ShellCompDirectiveNoFileComp + } + list, _ := container.ListSources() + return list, cobra.ShellCompDirectiveNoFileComp + }, + } +) + +func init() { + // Nothing to do here +} + +// GetRootCommand returns the root cobra.Command for the application. +func GetCommand() *cobra.Command { + return baseCmd +} diff --git a/internal/app/wwctl/container/root.go b/internal/app/wwctl/container/root.go index 363f82b5..6ffcf279 100644 --- a/internal/app/wwctl/container/root.go +++ b/internal/app/wwctl/container/root.go @@ -2,6 +2,7 @@ package container import ( "github.com/hpcng/warewulf/internal/app/wwctl/container/build" + "github.com/hpcng/warewulf/internal/app/wwctl/container/copy" "github.com/hpcng/warewulf/internal/app/wwctl/container/delete" "github.com/hpcng/warewulf/internal/app/wwctl/container/exec" "github.com/hpcng/warewulf/internal/app/wwctl/container/imprt" @@ -33,6 +34,7 @@ func init() { baseCmd.AddCommand(delete.GetCommand()) baseCmd.AddCommand(show.GetCommand()) baseCmd.AddCommand(syncuser.GetCommand()) + baseCmd.AddCommand(copy.GetCommand()) } diff --git a/internal/pkg/api/routes/v1/routes.proto b/internal/pkg/api/routes/v1/routes.proto index 83b844c5..e83389f4 100644 --- a/internal/pkg/api/routes/v1/routes.proto +++ b/internal/pkg/api/routes/v1/routes.proto @@ -32,6 +32,13 @@ message ContainerDeleteParameter { repeated string containerNames = 1; } +// ContainerCopyParameter contains 2 inputs : first one for the source container name and second one for the duplicated container name. +message ContainerCopyParameter { + repeated string containerSourceName = 1; + repeated string containerDestName = 1; + +} + // ContainerImportParameter has all input for importing a container. message ContainerImportParameter{ string source = 1; // container source uri diff --git a/internal/pkg/api/routes/wwapiv1/routes.pb.go b/internal/pkg/api/routes/wwapiv1/routes.pb.go index f13faca9..a3d34f7d 100644 --- a/internal/pkg/api/routes/wwapiv1/routes.pb.go +++ b/internal/pkg/api/routes/wwapiv1/routes.pb.go @@ -251,6 +251,16 @@ func (x *ContainerDeleteParameter) GetContainerNames() []string { return nil } +type ContainerCopyParameter struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ContainerSource string `protobuf:"bytes,1,rep,name=ContainerSource,proto3" json:"ContainerSource,omitempty"` + ContainerDestination string `protobuf:"bytes,1,rep,name=ContainerDestination,proto3" json:"ContainerDestination,omitempty"` +} + + // ContainerImportParameter has all input for importing a container. type ContainerImportParameter struct { state protoimpl.MessageState diff --git a/internal/pkg/container/build.go b/internal/pkg/container/build.go index cd3e45d9..e9392d82 100644 --- a/internal/pkg/container/build.go +++ b/internal/pkg/container/build.go @@ -37,7 +37,7 @@ func Build(name string, buildForce bool) error { } err := util.BuildFsImage( - "VNFS container " + name, + "VNFS container "+name, rootfsPath, imagePath, []string{"*"}, diff --git a/internal/pkg/container/util.go b/internal/pkg/container/util.go index 48211710..67ff9c48 100644 --- a/internal/pkg/container/util.go +++ b/internal/pkg/container/util.go @@ -48,14 +48,22 @@ func ListSources() ([]string, error) { return ret, nil } -func ValidSource(name string) bool { - fullPath := RootFsDir(name) +func DoesContainerExists(name string) bool { + fullPath := ImageFile(name) + return util.IsFile(fullPath) +} +func DoesSourceExist(name string) bool { + fullPath := RootFsDir(name) + return util.IsDir(fullPath) +} + +func ValidSource(name string) bool { if !ValidName(name) { return false } - if !util.IsDir(fullPath) { + if !DoesSourceExist(name) { wwlog.Verbose("Location is not a VNFS source directory: %s", name) return false } @@ -73,6 +81,23 @@ func DeleteSource(name string) error { return os.RemoveAll(fullPath) } +func Duplicate(name string, destination string) error { + fullPathImageSource := RootFsDir(name) + + wwlog.Info("Copying sources...") + err := ImportDirectory(fullPathImageSource, destination) + + if err != nil { + return err + } + wwlog.Info("Building container: %s", destination) + err = Build(destination, true) + if err != nil { + return err + } + return nil +} + /* Delete the image of a container */