Commit d7e13eb9 authored by songlinfeng's avatar songlinfeng
Browse files

add dtk-container-toolkit

parent fcdba4f3
# DTK Container Toolkit Changelog
## v1.2.3
bugfix:
- 去掉默认的ApparmorProfile,让挂载的HCU设备系统文件有可写权限。
## v1.2.2
RDMA:设置环境变量 `DTK_MOFED``enabled`时,开启RADM支持。
- 挂载发现的 MOFED Infiniband 设备
- --cap-add=SYS_LOCK
## v1.2.1
xprof 支持
- 挂载设备 /dev/mem
- 挂载HCU设备对应的PCI系统文件 /sys/bus/pci/devices/${PCIBusId}
- --cap-add=SYS_RAWIO
## v1.2.0
- 添加CDI支持
## v1.1.1
- 安装时自动更新docker的运行时
- 给容器添加/opt/hyhal链接
## v1.1.0
- initialize version
DOCKER ?= docker
MKDIR ?= mkdir
DIST_DIR ?= $(CURDIR)/dist
include $(CURDIR)/versions.mk
MODULE := dtk-container-toolkit
# By default run all native docker-based targets
docker-native:
include $(CURDIR)/docker/docker.mk
BUILDIMAGE_TAG ?= golang$(GOLANG_VERSION)
BUILDIMAGE ?= $(IMAGE_NAME)-build:$(BUILDIMAGE_TAG)
CMDS := $(patsubst ./cmd/%/,%,$(sort $(dir $(wildcard ./cmd/*/))))
CMD_TARGETS := $(patsubst %,cmd-%, $(CMDS))
CHECK_TARGETS := lint
MAKE_TARGETS := binaries build check fmt test examples cmds coverage generate licenses vendor check-vendor $(CHECK_TARGETS)
TARGETS := $(MAKE_TARGETS) $(CMD_TARGETS)
DOCKER_TARGETS := $(patsubst %,docker-%, $(TARGETS))
.PHONY: $(TARGETS) $(DOCKER_TARGETS)
ifeq ($(VERSION),)
CLI_VERSION = $(LIB_VERSION)$(if $(LIB_TAG),-$(LIB_TAG))
else
CLI_VERSION = $(VERSION)
endif
CLI_VERSION_PACKAGE = dtk-container-toolkit/internal/info
binaries: cmds
ifneq ($(PREFIX),)
cmd-%: COMMAND_BUILD_OPTIONS = -o $(PREFIX)/$(*)
endif
cmds: $(CMD_TARGETS)
ifneq ($(shell uname),Darwin)
EXTLDFLAGS = -Wl,--export-dynamic -Wl,--unresolved-symbols=ignore-in-object-files
else
EXTLDFLAGS = -Wl,-undefined,dynamic_lookup
endif
$(CMD_TARGETS): cmd-%:
go build -ldflags "-s -w '-extldflags=$(EXTLDFLAGS)' -X $(CLI_VERSION_PACKAGE).gitCommit=$(GIT_COMMIT) -X $(CLI_VERSION_PACKAGE).version=$(CLI_VERSION)" $(COMMAND_BUILD_OPTIONS) $(MODULE)/cmd/$(*)
build:
go build ./...
all: check test build binary
check: $(CHECK_TARGETS)
# Apply go fmt to the codebase
fmt:
go list -f '{{.Dir}}' $(MODULE)/... \
| xargs gofmt -s -l -w
# Apply goimports -local container-toolkit to the codebase
goimports:
go list -f {{.Dir}} $(MODULE)/... \
| xargs goimports -local $(MODULE) -w
lint:
golangci-lint run ./...
vendor:
go mod tidy
go mod vendor
go mod verify
check-vendor: vendor
git diff --quiet HEAD -- go.mod go.sum vendor
licenses:
go-licenses csv $(MODULE)/...
COVERAGE_FILE := coverage.out
test: build cmds
go test -coverprofile=$(COVERAGE_FILE) $(MODULE)/...
coverage: test
cat $(COVERAGE_FILE) | grep -v "_mock.go" > $(COVERAGE_FILE).no-mocks
go tool cover -func=$(COVERAGE_FILE).no-mocks
generate:
go generate $(MODULE)/...
# container-toolkit # DTK Container Toolkit
## 简介
DTK Container Toolkit 使用户能够构建和运行使用HCU设备的容器。该工具包包含一个容器运行时和一些用于自动配置容器以利用HCU的实用程序。
## 使用
首先确保已经安装好DTK。
### 安装
在 HCUOpt 发布仓库里找到适合系统的安装包,使用 dpkg/rpm -i 进行安装。
重启docker服务
```sh
$ sudo systemctl restart docker
```
### 在容器中使用HCU
#### 通过 docker CLI
可以通过 docker run 添加参数 --gpus 给容器添加HCU设备。
```sh
$ docker run -it --gpus all ubuntu:18.04 # 添加所有HCU设备
$ docker run -it --gpus 1 ubuntu:18.04 # 添加一个HCU设备,HCU 0
```
#### 通过环境变量 `DTK_VISIBLE_DEVICES`
可以通过 docker run 添加环境变量 -e DTK_VISIBLE_DEVICES 给容器添加HCU设备。
```sh
docker run -it -e DTK_VISIBLE_DEVICES=all ubuntu:18.04 # 添加所有HCU设备
docker run -it -e DTK_VISIBLE_DEVICES=0 ubuntu:18.04 # 添加HCU设备0
docker run -it -e DTK_VISIBLE_DEVICES=0,1 ubuntu:18.04 # 添加HCU设备0、1
```
/**
# Copyright (c) Advanced Micro Devices, Inc. All rights reserved.
#
# 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 main
import (
"fmt"
"os"
"developer.sourcefind.cn/codes/songlinfeng/container-toolkit/cmd/dcu-ctk/runtime"
"developer.sourcefind.cn/codes/songlinfeng/container-toolkit/internal/logger"
"github.com/urfave/cli/v2"
)
var (
Version = "dev"
BuildDate = "unknown"
GitCommit = "none"
)
func showVersion() *cli.Command {
showVersionCmd := cli.Command{
Name: "version",
Usage: "Show the version",
Action: func(c *cli.Context) error {
fmt.Printf("Version: %s\nBuild Date: %s\nGit Commit: %s\n", Version, BuildDate, GitCommit)
return nil
},
}
return &showVersionCmd
}
func main() {
logger.Init(false)
//Create the top-level CLI tree
dcuCtkCli := &cli.App{
Name: "DCU Container Toolkit CLI",
EnableBashCompletion: true,
Usage: "Tool to configure DCU Container Toolkit",
}
// Add subcommands
dcuCtkCli.Commands = []*cli.Command{
showVersion(),
runtime.AddNewCommand(),
//cdi.AddNewCommand(),
}
err := dcuCtkCli.Run(os.Args)
if err != nil {
fmt.Printf("%v\n", err)
os.Exit(1)
}
}
\ No newline at end of file
package main
import (
"bytes"
"encoding/json"
"fmt"
"os"
"os/exec"
"strings"
"testing"
)
const (
configFile = "/tmp/testConfig.json"
dcuRuntimePath = "dcu-container-runtime"
dcuRuntimeName = "dcu"
removeAndDefErrMsg = "remove flag cannot be used along with set-as-default flag"
setUnsetDefErrMsg = "both set and unset as default cannot be used at the same time"
)
var cliPath = os.Getenv("DCU_CTK_PATH")
//Helper function to run the CLI command and return the output/error
func runCLI(args ...string)(string, string, error) {
cmd := exec.Command(cliPath, args...)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
return stdout.String(), stderr.String(), err
}
func setup(t *testing.T){
if cliPath == "" {
t.Fatalf("cliPath is not set, usage: export DCU_CTK_PATH=<path to dcu-ctk executable>; go test -v")
}
if _, err := os.Stat(cliPath); os.IsNotExist(err) {
t.Fatalf("dcu-ctk is not built, please run 'make container-toolkit-ctk'")
}
}
func verifyConfigFile(t *testing.T, isDefault bool, isEmpty bool) {
type features struct{
Cdi bool `json:"cdi"`
}
type runtimeConfig struct {
Args []string `json:"args"`
Path string `json:"path"`
}
type runtimes map[string]runtimeConfig
type config struct {
DefaultRuntime string `json:"defaultRuntime"`
Features features `json:"features"`
Runtimes runtimes `json:"runtimes"`
}
// read the configFile
_, err := os.Stat(configFile)
Assert(t, os.IsNotExist(err) == false,fmt.Sprintf("config file: %v doesn't exist", configFile))
cfg := config{}
fmt.Printf("Loading configuration from: %v\n", configFile)
readB, err := os.ReadFile(configFile)
Assert(t, err == nil, fmt.Sprintf("Error reading config file: %v, err: %v", configFile, err))
reader := bytes.NewReader(readB)
err = json.NewDecoder(reader).Decode(&cfg)
Assert(t, err == nil, fmt.Sprintf("Error decoding file: %v, err: %v", configFile, err))
fmt.Printf("config: %+v\n", cfg)
if isEmpty {
// verify the config is removed
//Assert(t, cfg.Features.Cdi == false, "CDI is enabled in the config file")
Assert(t, len(cfg.Runtimes) == 0, "Number of runtimes in config is not 0")
Assert(t, cfg.DefaultRuntime == "", fmt.Sprintf("default runtime should not be set to %v", cfg.DefaultRuntime))
} else {
//Assert(t, cfg.Features.Cdi == true, "CDI is not enabled in the config file")
Assert(t, len(cfg.Runtimes) == 1, "Number of runtimes in config is not 1")
rtime, exists := cfg.Runtimes["dcu"]
Assert(t, exists == true, "dcu runtime doesn't exist in the config file")
Assert(t, rtime.Path == dcuRuntimePath, fmt.Sprintf("dcu runtime path isn't set to %v", dcuRuntimePath))
if isDefault {
Assert(t, cfg.DefaultRuntime == dcuRuntimeName, fmt.Sprintf("default runtime not set to %v", dcuRuntimeName))
} else {
Assert(t, cfg.DefaultRuntime == "", fmt.Sprintf("default runtime should not be set to %v", cfg.DefaultRuntime))
}
}
}
func Assert(t *testing.T, b bool, errString string) {
if !b{
t.Errorf(errString)
}
}
func cleanUp(t *testing.T) {
fmt.Printf("Deleting file: %v\n", configFile)
os.Remove(configFile)
}
func TestConfigureRunTimeAddRemove(t *testing.T) {
fmt.Printf("dcu-ctk path: %v\n", cliPath)
setup(t)
cfgPathArg := "--config-path=" + configFile
// add dcu to runtimes
out, outErr, err := runCLI("runtime", "configure", "--runtime=docker", cfgPathArg)
Assert(t, outErr == "", fmt.Sprintf("dcu-ctk runtime configure returned err: %v", outErr))
Assert(t, err == nil, fmt.Sprintf("Error running dcu-ctk err: %v", err))
fmt.Println("output: ", out)
verifyConfigFile(t, false, false)
// remove dcu from runtimes
out, outErr, err = runCLI("runtime", "configure", "--runtime=docker", cfgPathArg, "--remove")
Assert(t, outErr == "", fmt.Sprintf("dcu-ctk runtime configure remove returned err: %v", outErr))
Assert(t, err == nil, fmt.Sprintf("Error running dcu-ctk err: %v", err))
fmt.Println("output: ", out)
verifyConfigFile(t, false, true)
cleanUp()
}
func TestConfigureRunTimeDefault(t *testing.T) {
fmt.Printf("dcu-ctk path: %v\n", cliPath)
setup(t)
cfgPathArg := "--config-path=" + configFile
// add dcu to runtimes
out, outErr, err := runCLI("runtime", "configure", "--runtime=docker", cfgPathArg, "--set-as-default")
Assert(t, outErr == "", fmt.Sprintf("dcu-ctk runtime configure as default returned err: %v", outErr))
Assert(t, err == nil, fmt.Sprintf("Error running dcu-ctk err: %v", err))
fmt.Println("output: ", out)
verifyConfigFile(t, true, false)
// unset as default
out, outErr, err = runCLI("runtime", "configure", "--runtime=docker", cfgPathArg, "--unset-as-default")
Assert(t, outErr == "", fmt.Sprintf("dcu-ctk runtime configure unset default returned err: %v", outErr))
Assert(t, err == nil, fmt.Sprintf("Error running dcu-ctk err: %v", err))
fmt.Println("output: ", out)
verifyConfigFile(t, false, false)
// add it back
out, outErr, err = runCLI("runtime", "configure", "--runtime=docker", cfgPathArg, "--set-as-default")
Assert(t, outErr == "", fmt.Sprintf("dcu-ctk runtime configure as default returned err: %v", outErr))
Assert(t, err == nil, fmt.Sprintf("Error running dcu-ctk err: %v", err))
fmt.Println("output: ", out)
verifyConfigFile(t, true, false)
// use remove flag and make sure default gets deleted too
out, outErr, err = runCLI("runtime", "configure", "--runtime=docker", cfgPathArg, "--remove")
Assert(t, outErr == "", fmt.Sprintf("dcu-ctk runtime configure unset default returned err: %v", outErr))
Assert(t, err == nil, fmt.Sprintf("Error running amd-ctk err: %v", err))
fmt.Println("output: ", out)
verifyConfigFile(t, false, true)
cleanUp()
}
func TestConfigureRuntimeMultiFlags(t *testing.T) {
fmt.Printf("dcu-ctk path: %v\n", cliPath)
setup(t)
cfgPathArg := "--config-path=" + configFile
// add amd to runtimes as default along with remove flag
out, outErr, err := runCLI("runtime", "configure", "--runtime=docker", cfgPathArg, "--remove", "--set-as-default")
Assert(t, outErr == "", fmt.Sprintf("dcu-ctk runtime configure as default returned err: %v", outErr))
// shoudl error
Assert(t, err != nil, "err shouldn't be nil")
// match the error message
Assert(t, strings.TrimSpace(out) == removeAndDefErrMsg, fmt.Sprintf("stdout: %v should have been '%v'", out, removeAndDefErrMsg))
// use set default and unset default flags at the same time
out, outErr, err = runCLI("runtime", "configure", "--runtime=docker", cfgPathArg, "--unset-as-default", "--set-as-default")
Assert(t, outErr == "", fmt.Sprintf("dcu-ctk runtime configure as default returned err: %v", outErr))
// shoudl error
Assert(t, err != nil, "err shouldn't be nil")
// match the error message
Assert(t, strings.TrimSpace(out) == setUnsetDefErrMsg, fmt.Sprintf("stdout: %v should have been '%v'", out, setUnsetDefErrMsg))
cleanUp()
}
\ No newline at end of file
/**
# Copyright (c) Advanced Micro Devices, Inc. All rights reserved.
#
# 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 configure
import (
"fmt"
"developer.sourcefind.cn/codes/songlinfeng/container-toolkit/cmd/dcu-ctk/runtime/engine"
"developer.sourcefind.cn/codes/songlinfeng/container-toolkit/cmd/dcu-ctk/runtime/engine/docker"
"github.com/urfave/cli/v2"
)
const (
defaultRuntime = "docker"
defaultDcuRuntimeName = "dcu"
defaultDcuRuntimeExecutable = "dcu-container-runtime"
defaultDockerConfigFilePath = "/etc/docker/daemon.json"
)
type configOptions struct {
runtime string
configFilepath string
setAsDefault bool
unSetAsDefault bool
remove bool
}
func AddNewCommand() *cli.Command {
cfgOptions := configOptions{}
// Add the configure subcommand
configureCmd := cli.Command{
Name: "configure",
Usage: "Configure a runtime to the container engine",
Before: func(c *cli.Context) error {
return validateConfigOptions(c, &cfgOptions)
},
Action: func(c *cli.Context) error {
return performAction(c, &cfgOptions)
},
}
configureCmd.Flags = []cli.Flag{
&cli.StringFlag{
Name: "runtime",
Usage: "target runtime engine, [docker for now]",
Value: defaultRuntime,
Destination: &cfgOptions.runtime,
},
&cli.BoolFlag{
Name: "remove",
Usage: "remove from target runtimes",
Destination: &cfgOptions.remove,
},
&cli.StringFlag{
Name: "config-path",
Usage: "path to the configuration file for the target engine",
Value: defaultDockerConfigFilePath,
Destination: &cfgOptions.configFilepath,
},
&cli.BoolFlag{
Name: "dcu-set-as-default",
Aliases: []string{"set-as-default"},
Usage: "set DCU runtime as the default",
Destination: &cfgOptions.setAsDefault,
},
&cli.BoolFlag{
Name: "unset-dcu-as-default",
Aliases: []string{"unset-as-default"},
Usage: "remove DCU runtime as the default",
Destination: &cfgOptions.unSetAsDefault,
},
}
return &configureCmd
}
func validateConfigOptions(c *cli.Context, cfgOptions *configOptions) error {
if cfgOptions.runtime != "docker" {
return fmt.Errorf("unsupported runtime engine: %v", cfgOptions.runtime)
}
if cfgOptions.setAsDefault && cfgOptions.unSetAsDefault {
return fmt.Errorf("both set and unset as default cannot be used at the same time")
}
if cfgOptions.remove {
if cfgOptions.setAsDefault || cfgOptions.unSetAsDefault {
return fmt.Errorf("remove flag cannot be used along with set-as-default flag")
}
}
return nil
}
func performAction(c *cli.Context, cfgOptions *configOptions) error {
var (
err error
runtimeEngine engine.Interface
doNotUpdate bool
)
switch cfgOptions.runtime {
case "docker":
runtimeEngine, err = docker.New(cfgOptions.configFilepath)
default:
return fmt.Errorf("unsupported runtime engine: %v", cfgOptions.runtime)
}
if err != nil || runtimeEngine == nil {
return fmt.Errorf("failed to init config for runtime engine: %v | err: %v", cfgOptions.runtime, err)
}
if cfgOptions.unSetAsDefault {
err = runtimeEngine.UnsetDefaultRuntime()
if err != nil {
return fmt.Errorf("failed to unset default runtime: %v", err)
}
} else {
if cfgOptions.remove {
err, doNotUpdate = runtimeEngine.RemoveRuntime(defaultDcuRuntimeName)
} else {
err = runtimeEngine.ConfigRuntime(defaultDcuRuntimeName, defaultDcuRuntimeExecutable, cfgOptions.setAsDefault)
}
if err != nil {
return fmt.Errorf("failed to update configuration: %v", err)
}
}
// Save the config
if !doNotUpdate {
num, err := runtimeEngine.Update(cfgOptions.configFilepath)
if err != nil {
return fmt.Errorf("failed to save the config: %v", err)
}
if num != 0 {
fmt.Printf("Updated the config file: %v\n", cfgOptions.configFilepath)
}
fmt.Printf("Please restart %v daemon\n", cfgOptions.runtime)
}
return nil
}
\ No newline at end of file
/**
# Copyright (c) Advanced Micro Devices, Inc. All rights reserved.
#
# 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 docker
import (
"bytes"
"encoding/json"
"fmt"
"os"
)
const (
runtimesKey = "runtimes"
defaultRuntimeKey = "default-runtime"
featuresKey = "features"
//defaultCDISpecPath = "/etc/cdi"
)
type dockerConfig map[string]interface{}
func New(path string) (*dockerConfig, error){
return loadConfigFile(path)
}
func loadConfigFile(path string) (*dockerConfig, error) {
//check if the file exists
f, err := os.Stat(path)
if os.IsExist(err) && f.IsDir(){
return nil, fmt.Errorf("file path is a directory")
}
config := dockerConfig{}
if os.IsNotExist(err) {
//return empty config
return &config, nil
}
fmt.Printf("Loading configuration from: %v\n", path)
readB, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("error reading configuration file: %v | err: %v", path, err)
}
reader := bytes.NewReader(readB)
err = json.NewDecoder(reader).Decode(&config)
if err != nil {
return nil, fmt.Errorf("error decoding configuration file: %v | err: %v", path, err)
}
return &config, nil
}
func (d *dockerConfig) ConfigRuntime(name string, path string, isDefault bool) error {
if d == nil {
return fmt.Errorf("configuration is empty")
}
currentCfg := *d
//check any existing "runtimes"
runtimes := map[string]interface{}{}
if _, exists := currentCfg[runtimesKey]; exists {
runtimes = currentCfg[runtimesKey].(map[string]interface{})
}
runtimes[name] = map[string]interface{}{
"path": path,
"args": []string{},
}
currentCfg[runtimesKey] = runtimes
// Enable CDI by default
/*
currentCfg[featuresKey] = map[string]interface{}{
"cdi": true,
}
*/
if isDefault {
currentCfg[defaultRuntimeKey] = name
}
*d = currentCfg
return nil
}
// RemoveRuntime removes the runtime configuration and returns
// an error and a do not update flag in case daemon.json doesn't need
// to be updated
func (d *dockerConfig) RemoveRuntime(name string) (error, bool){
//_ = os.RemoveAll(defaultCDISpecPath)
if d == nil {
return fmt.Errorf("configuration is empyt"), true
}
updated := false
currentCfg := *daemon
//check any existing "runtimes"
if _, exists := currentCfg[runtimesKey]; exists{
runtimes := currentCfg[runtimesKey].(map[string]interface{})
delete(runtimes, name)
delete(currentCfg, featuresKey)
_, defExists := currentCfg[defaultRuntimeKey]
if defExists {
defCfg := currentCfg[defaultRuntimeKey].(string)
if defCfg == name {
delete(currentCfg, defaultRuntimeKey)
}
}
if len(runtimes) == 0 {
delete(currentCfg, runtimesKey)
}
updated = true
}
if updated {
*d = currentCfg
return nil, false
}
return nil, true
}
func (d dockerConfig) Update(path string) (int, error) {
toWrite, err := json.MarshalIndent(d, ""," ")
if err != nil{
return 0, fmt.Errorf("json marshal failed: %v", err)
}
if path == "" {
num, err := os.Stdout.Write(toWrite)
return num, err
}
f, err := os.Create(path)
if err != nil {
return 0, fmt.Errorf("failed to open file: %v | err: %v", path, err)
}
defer f.Close()
return f.Write(toWrite)
}
}
\ No newline at end of file
/**
# Copyright (c) Advanced Micro Devices, Inc. All rights reserved.
#
# 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 engine
type Interface interface {
ConfigRuntime(string, string, bool) error
UnsetDefaultRuntime() error
Update(string) (int, error)
RemoveRuntime(string) (error, bool)
}
\ No newline at end of file
/**
# Copyright (c) Advanced Micro Devices, Inc. All rights reserved.
#
# 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 runtime
import (
"developer.sourcefind.cn/codes/songlinfeng/container-toolkit/cmd/dcu-ctk/runtime/configure"
"github.com/urfave/cli/v2"
)
func AddNewCommand() *cli.Command {
// Add the runtime command
runtimeCmd := cli.Command{
Name: "runtime",
Usage: "runtime related commands for AMD Container Toolkit",
}
runtimeCmd.Subcommands = []*cli.Command{
configure.AddNewCommand(),
}
return &runtimeCmd
}
/**
# Copyright (c) 2024, HCUOpt CORPORATION. All rights reserved.
**/
package chmod
import (
"dtk-container-toolkit/internal/logger"
"dtk-container-toolkit/internal/oci"
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/urfave/cli/v2"
)
type command struct {
logger logger.Interface
}
type config struct {
paths cli.StringSlice
modeStr string
mode fs.FileMode
containerSpec string
}
// NewCommand constructs a chmod command with the specified logger
func NewCommand(logger logger.Interface) *cli.Command {
c := command{
logger: logger,
}
return c.build()
}
// build the chmod command
func (m command) build() *cli.Command {
cfg := config{}
// Create the 'chmod' command
c := cli.Command{
Name: "chmod",
Usage: "Set the permissions of folders in the container by running chmod. The container root is prefixed to the specified paths.",
Before: func(c *cli.Context) error {
return validateFlags(c, &cfg)
},
Action: func(c *cli.Context) error {
return m.run(c, &cfg)
},
}
c.Flags = []cli.Flag{
&cli.StringSliceFlag{
Name: "path",
Usage: "Specify a path to apply the specified mode to",
Destination: &cfg.paths,
},
&cli.StringFlag{
Name: "mode",
Usage: "Specify the file mode",
Destination: &cfg.modeStr,
},
&cli.StringFlag{
Name: "container-spec",
Usage: "Specify the path to the OCI container spec. If empty or '-' the spec will be read from STDIN",
Destination: &cfg.containerSpec,
},
}
return &c
}
func validateFlags(c *cli.Context, cfg *config) error {
if strings.TrimSpace(cfg.modeStr) == "" {
return fmt.Errorf("a non-empty mode must be specified")
}
modeInt, err := strconv.ParseUint(cfg.modeStr, 8, 32)
if err != nil {
return fmt.Errorf("failed to parse mode as octal: %v", err)
}
cfg.mode = fs.FileMode(modeInt)
for _, p := range cfg.paths.Value() {
if strings.TrimSpace(p) == "" {
return fmt.Errorf("paths must not be empty")
}
}
return nil
}
func (m command) run(c *cli.Context, cfg *config) error {
s, err := oci.LoadContainerState(cfg.containerSpec)
if err != nil {
return fmt.Errorf("failed to load container state: %v", err)
}
containerRoot, err := s.GetContainerRoot()
if err != nil {
return fmt.Errorf("failed to determined container root: %v", err)
}
if containerRoot == "" {
return fmt.Errorf("empty container root detected")
}
paths := m.getPaths(containerRoot, cfg.paths.Value(), cfg.mode)
if len(paths) == 0 {
m.logger.Debugf("No paths specified; exiting")
return nil
}
for _, path := range paths {
err = os.Chmod(path, cfg.mode)
// in some cases this is not an issue (e.g. whole /dev mounted), see #143
if errors.Is(err, fs.ErrPermission) {
m.logger.Debugf("Ignoring permission error with chmod: %v", err)
err = nil
}
}
return err
}
// getPaths updates the specified paths relative to the root.
func (m command) getPaths(root string, paths []string, desiredMode fs.FileMode) []string {
var pathsInRoot []string
for _, f := range paths {
path := filepath.Join(root, f)
stat, err := os.Stat(path)
if err != nil {
m.logger.Debugf("Skipping path %q: %v", path, err)
continue
}
if (stat.Mode()&(fs.ModePerm|fs.ModeSetuid|fs.ModeSetgid|fs.ModeSticky))^desiredMode == 0 {
m.logger.Debugf("Skipping path %q: already desired mode", path)
continue
}
pathsInRoot = append(pathsInRoot, path)
}
return pathsInRoot
}
/**
# Copyright (c) 2024, HCUOpt CORPORATION. All rights reserved.
**/
package commands
import (
"dtk-container-toolkit/cmd/dtk-cdi-hook/chmod"
symlinks "dtk-container-toolkit/cmd/dtk-cdi-hook/create-symlinks"
"dtk-container-toolkit/internal/logger"
"github.com/urfave/cli/v2"
)
// New creates the commands associated with supported CDI hooks.
// These are shared by the dtk-cdi-hook and dtk-ctk hook commands.
func New(logger logger.Interface) []*cli.Command {
return []*cli.Command{
symlinks.NewCommand(logger),
chmod.NewCommand(logger),
}
}
/**
# Copyright (c) 2024, HCUOpt CORPORATION. All rights reserved.
**/
package symlinks
import (
"dtk-container-toolkit/internal/logger"
"dtk-container-toolkit/internal/oci"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/urfave/cli/v2"
)
type command struct {
logger logger.Interface
}
type config struct {
hostRoot string
links cli.StringSlice
containerSpec string
}
// NewCommand constructs a hook command with the specified logger
func NewCommand(logger logger.Interface) *cli.Command {
c := command{
logger: logger,
}
return c.build()
}
// build
func (m command) build() *cli.Command {
cfg := config{}
// Create the '' command
c := cli.Command{
Name: "create-symlinks",
Usage: "A hook to create symlinks in the container. This can be used to process CSV mount specs",
Action: func(c *cli.Context) error {
return m.run(c, &cfg)
},
}
c.Flags = []cli.Flag{
&cli.StringFlag{
Name: "host-root",
Usage: "The root on the host filesystem to use to resolve symlinks",
Destination: &cfg.hostRoot,
},
&cli.StringSliceFlag{
Name: "link",
Usage: "Specify a specific link to create. The link is specified as target::link",
Destination: &cfg.links,
},
&cli.StringFlag{
Name: "container-spec",
Usage: "Specify the path to the OCI container spec. If empty or '-' the spec will be read from STDIN",
Destination: &cfg.containerSpec,
},
}
return &c
}
func (m command) run(c *cli.Context, cfg *config) error {
s, err := oci.LoadContainerState(cfg.containerSpec)
if err != nil {
return fmt.Errorf("failed to load container state: %v", err)
}
containerRoot, err := s.GetContainerRoot()
if err != nil {
return fmt.Errorf("failed to determined container root: %v", err)
}
created := make(map[string]bool)
links := cfg.links.Value()
for _, l := range links {
parts := strings.Split(l, "::")
if len(parts) != 2 {
m.logger.Warningf("Invalid link specification %v", l)
continue
}
err := m.createLink(created, cfg.hostRoot, containerRoot, parts[0], parts[1])
if err != nil {
m.logger.Warningf("Failed to create link %v: %v", parts, err)
}
}
return nil
}
func (m command) createLink(created map[string]bool, hostRoot string, containerRoot string, target string, link string) error {
linkPath, err := changeRoot(hostRoot, containerRoot, link)
if err != nil {
m.logger.Warningf("Failed to resolve path for link %v relative to %v: %v", link, containerRoot, err)
}
if created[linkPath] {
m.logger.Debugf("Link %v already created", linkPath)
return nil
}
targetPath, err := changeRoot(hostRoot, "/", target)
if err != nil {
m.logger.Warningf("Failed to resolve path for target %v relative to %v: %v", target, "/", err)
}
m.logger.Infof("Symlinking %v to %v", linkPath, targetPath)
err = os.MkdirAll(filepath.Dir(linkPath), 0755)
if err != nil {
return fmt.Errorf("failed to create directory: %v", err)
}
err = os.Symlink(target, linkPath)
if err != nil {
return fmt.Errorf("failed to create symlink: %v", err)
}
return nil
}
func changeRoot(current string, new string, path string) (string, error) {
if !filepath.IsAbs(path) {
return path, nil
}
relative := path
if current != "" {
r, err := filepath.Rel(current, path)
if err != nil {
return "", err
}
relative = r
}
return filepath.Join(new, relative), nil
}
/**
# Copyright (c) 2024, HCUOpt CORPORATION. All rights reserved.
**/
package main
import (
"dtk-container-toolkit/cmd/dtk-cdi-hook/commands"
"dtk-container-toolkit/internal/info"
"os"
"github.com/sirupsen/logrus"
cli "github.com/urfave/cli/v2"
)
// options defines the options that can be set for the CLI through config files,
// environment variables, or command line flags
type options struct {
// Debug indicates whether the CLI is started in "debug" mode
Debug bool
// Quiet indicates whether the CLI is started in "quiet" mode
Quiet bool
}
func main() {
logger := logrus.New()
// Create a options struct to hold the parsed environment variables or command line flags
opts := options{}
// Create the top-level CLI
c := cli.NewApp()
c.Name = "C-3000 DTK CDI Hook"
c.UseShortOptionHandling = true
c.EnableBashCompletion = true
c.Usage = "Command to structure files for usage inside a container, called as hooks from a container runtime, defined in a CDI yaml file"
c.Version = info.GetVersionString()
// Setup the flags for this command
c.Flags = []cli.Flag{
&cli.BoolFlag{
Name: "debug",
Aliases: []string{"d"},
Usage: "Enable debug-level logging",
Destination: &opts.Debug,
EnvVars: []string{"DTK_CDI_DEBUG"},
},
&cli.BoolFlag{
Name: "quiet",
Usage: "Suppress all output except for errors; overrides --debug",
Destination: &opts.Quiet,
EnvVars: []string{"DTK_CDI_QUIET"},
},
}
// Set log-level for all subcommands
c.Before = func(c *cli.Context) error {
logLevel := logrus.InfoLevel
if opts.Debug {
logLevel = logrus.DebugLevel
}
if opts.Quiet {
logLevel = logrus.ErrorLevel
}
logger.SetLevel(logLevel)
return nil
}
// Define the subcommands
c.Commands = commands.New(logger)
// Run the CLI
err := c.Run(os.Args)
if err != nil {
logger.Errorf("%v", err)
os.Exit(1)
}
}
# DTK Container Runtime
DTK Container Runtime 是一个专为符合 OCI (Open Container Initiative,开放容器计划)规范的底层运行时( 如[runc](https://github.com/opencontainers/runc) )设计的垫片(shim)。当 dtk-container-runtime 接收到一个 `create` 命令时,它会对传入的 [OCI运行时规范](https://github.com/opencontainers/runtime-spec) 进行即时修改,以添加对 HCU 的特殊支持,然后将这个修改后的命令转发给底层的运行时(如runc、containerd等)。
## 配置
DTK Container Runtime (DCR) 使用基于文件的配置方式,其配置文件存储在 /etc/dtk-container-runtime/config.toml 路径下。安装 dtk-container-toolkit 时会自动生成该文件。
/**
# Copyright (c) 2024, HCUOpt CORPORATION. All rights reserved.
**/
package main
import (
"dtk-container-toolkit/internal/runtime"
"os"
)
func main() {
r := runtime.New()
err := r.Run(os.Args)
if err != nil {
os.Exit(1)
}
}
/**
# Copyright (c) 2024, HCUOpt CORPORATION. All rights reserved.
**/
package cdi
import (
"dtk-container-toolkit/cmd/dtk-ctk/cdi/generate"
"dtk-container-toolkit/cmd/dtk-ctk/cdi/list"
"dtk-container-toolkit/cmd/dtk-ctk/cdi/transform"
"dtk-container-toolkit/internal/logger"
"github.com/urfave/cli/v2"
)
type command struct {
logger logger.Interface
}
// NewCommand constructs an info command with the specified logger
func NewCommand(logger logger.Interface) *cli.Command {
c := command{
logger: logger,
}
return c.build()
}
// build
func (m command) build() *cli.Command {
// Create the 'hook' command
hook := cli.Command{
Name: "cdi",
Usage: "Provide tools for interacting with Container Device Interface specifications",
}
hook.Subcommands = []*cli.Command{
generate.NewCommand(m.logger),
transform.NewCommand(m.logger),
list.NewCommand(m.logger),
}
return &hook
}
/**
# Copyright (c) 2024, HCUOpt CORPORATION. All rights reserved.
**/
package generate
import (
"dtk-container-toolkit/internal/config"
"dtk-container-toolkit/internal/logger"
"dtk-container-toolkit/pkg/c3000cdi"
"dtk-container-toolkit/pkg/c3000cdi/spec"
"dtk-container-toolkit/pkg/c3000cdi/transform"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/urfave/cli/v2"
"tags.cncf.io/container-device-interface/pkg/parser"
)
const (
allDeviceName = "all"
)
type command struct {
logger logger.Interface
}
type options struct {
output string
format string
deviceNameStrategies cli.StringSlice
driverRoot string
devRoot string
dtkCDIHookPath string
ldconfigPath string
mode string
vendor string
class string
configSearchPaths cli.StringSlice
librarySearchPaths cli.StringSlice
csv struct {
files cli.StringSlice
ignorePatterns cli.StringSlice
}
}
// NewCommand constructs a generate-cdi command with the specified logger
func NewCommand(logger logger.Interface) *cli.Command {
c := command{
logger: logger,
}
return c.build()
}
// build creates the CLI command
func (m command) build() *cli.Command {
opts := options{}
// Create the 'generate-cdi' command
c := cli.Command{
Name: "generate",
Usage: "Generate CDI specifications for use with CDI-enabled runtimes",
Before: func(c *cli.Context) error {
return m.validateFlags(c, &opts)
},
Action: func(c *cli.Context) error {
return m.run(c, &opts)
},
}
c.Flags = []cli.Flag{
&cli.StringSliceFlag{
Name: "config-search-path",
Usage: "Specify the path to search for config files when discovering the entities that should be included in the CDI specification.",
Destination: &opts.configSearchPaths,
},
&cli.StringFlag{
Name: "output",
Usage: "Specify the file to output the generated CDI specification to. If this is '' the specification is output to STDOUT",
Destination: &opts.output,
},
&cli.StringFlag{
Name: "format",
Usage: "The output format for the generated spec [json | yaml]. This overrides the format defined by the output file extension (if specified).",
Value: spec.FormatYAML,
Destination: &opts.format,
},
&cli.StringFlag{
Name: "mode",
Aliases: []string{"discovery-mode"},
Usage: "The mode to use when discovering the available entities. " +
"One of [" + strings.Join(c3000cdi.AllModes[string](), " | ") + "]. " +
"If mode is set to 'auto' the mode will be determined based on the system configuration.",
Value: string(c3000cdi.ModeAuto),
Destination: &opts.mode,
},
&cli.StringSliceFlag{
Name: "device-name-strategy",
Usage: "Specify the strategy for generating device names. If this is specified multiple times, the devices will be duplicated for each strategy. One of [index | uuid | type-index]",
Value: cli.NewStringSlice(c3000cdi.DeviceNameStrategyIndex, c3000cdi.DeviceNameStrategyUUID),
Destination: &opts.deviceNameStrategies,
},
&cli.StringSliceFlag{
Name: "library-search-path",
Usage: "Specify the path to search for libraries when discovering the entities that should be included in the CDI specification.\n\tNote: This option only applies to CSV mode.",
Destination: &opts.librarySearchPaths,
},
&cli.StringFlag{
Name: "dtk-cdi-hook-path",
Aliases: []string{"dtk-ctk-path"},
Usage: "Specify the path to use for the dtk-cdi-hook in the generated CDI specification. " +
"If not specified, the PATH will be searched for `dtk-cdi-hook`. " +
"NOTE: That if this is specified as `dtk-ctk`, the PATH will be searched for `dtk-ctk` instead.",
Destination: &opts.dtkCDIHookPath,
},
&cli.StringFlag{
Name: "ldconfig-path",
Usage: "Specify the path to use for ldconfig in the generated CDI specification",
Destination: &opts.ldconfigPath,
},
&cli.StringFlag{
Name: "vendor",
Aliases: []string{"cdi-vendor"},
Usage: "the vendor string to use for the generated CDI specification.",
Value: "c-3000.com",
Destination: &opts.vendor,
},
&cli.StringFlag{
Name: "class",
Aliases: []string{"cdi-class"},
Usage: "the class string to use for the generated CDI specification.",
Value: "hcu",
Destination: &opts.class,
},
}
return &c
}
func (m command) validateFlags(c *cli.Context, opts *options) error {
opts.format = strings.ToLower(opts.format)
switch opts.format {
case spec.FormatJSON:
case spec.FormatYAML:
default:
return fmt.Errorf("invalid output format: %v", opts.format)
}
opts.mode = strings.ToLower(opts.mode)
if !c3000cdi.IsValidMode(opts.mode) {
return fmt.Errorf("invalid discovery mode: %v", opts.mode)
}
for _, strategy := range opts.deviceNameStrategies.Value() {
_, err := c3000cdi.NewDeviceNamer(strategy)
if err != nil {
return err
}
}
opts.dtkCDIHookPath = config.ResolveDTKCDIHookPath(m.logger, opts.dtkCDIHookPath)
if outputFileFormat := formatFromFilename(opts.output); outputFileFormat != "" {
m.logger.Debugf("Inferred output format as %q from output file name", outputFileFormat)
if !c.IsSet("format") {
opts.format = outputFileFormat
} else if outputFileFormat != opts.format {
m.logger.Warningf("Requested output format %q does not match format implied by output file name: %q", opts.format, outputFileFormat)
}
}
if err := parser.ValidateVendorName(opts.vendor); err != nil {
return fmt.Errorf("invalid CDI vendor name: %v", err)
}
if err := parser.ValidateClassName(opts.class); err != nil {
return fmt.Errorf("invalid CDI class name: %v", err)
}
return nil
}
func (m command) run(c *cli.Context, opts *options) error {
spec, err := m.generateSpec(opts)
if err != nil {
return fmt.Errorf("failed to generate CDI spec: %v", err)
}
m.logger.Infof("Generated CDI spec with version %v", spec.Raw().Version)
if opts.output == "" {
_, err := spec.WriteTo(os.Stdout)
if err != nil {
return fmt.Errorf("failed to write CDI spec to STDOUT: %v", err)
}
return nil
}
return spec.Save(opts.output)
}
func formatFromFilename(filename string) string {
ext := filepath.Ext(filename)
switch strings.ToLower(ext) {
case ".json":
return spec.FormatJSON
case ".yaml", ".yml":
return spec.FormatYAML
}
return ""
}
func (m command) generateSpec(opts *options) (spec.Interface, error) {
var deviceNamers []c3000cdi.DeviceNamer
for _, strategy := range opts.deviceNameStrategies.Value() {
deviceNamer, err := c3000cdi.NewDeviceNamer(strategy)
if err != nil {
return nil, fmt.Errorf("failed to create device namer: %v", err)
}
deviceNamers = append(deviceNamers, deviceNamer)
}
cdilib, err := c3000cdi.New(
c3000cdi.WithLogger(m.logger),
c3000cdi.WithDriverRoot(opts.driverRoot),
c3000cdi.WithDevRoot(opts.devRoot),
c3000cdi.WithDTKCDIHookPath(opts.dtkCDIHookPath),
c3000cdi.WithLdconfigPath(opts.ldconfigPath),
c3000cdi.WithDeviceNamers(deviceNamers...),
c3000cdi.WithMode(opts.mode),
c3000cdi.WithConfigSearchPaths(opts.configSearchPaths.Value()),
c3000cdi.WithLibrarySearchPaths(opts.librarySearchPaths.Value()),
)
if err != nil {
return nil, fmt.Errorf("failed to create CDI library: %v", err)
}
deviceSpecs, err := cdilib.GetAllDeviceSpecs()
if err != nil {
return nil, fmt.Errorf("failed to create device CDI specs: %v", err)
}
commonEdits, err := cdilib.GetCommonEdits()
if err != nil {
return nil, fmt.Errorf("failed to create edits common for entities: %v", err)
}
return spec.New(
spec.WithVendor(opts.vendor),
spec.WithClass(opts.class),
spec.WithDeviceSpecs(deviceSpecs),
spec.WithEdits(*commonEdits.ContainerEdits),
spec.WithFormat(opts.format),
spec.WithMergedDeviceOptions(
transform.WithName(allDeviceName),
transform.WithSkipIfExists(true),
),
spec.WithPermissions(0644),
)
}
/**
# Copyright (c) 2024, HCUOpt CORPORATION. All rights reserved.
**/
package list
import (
"dtk-container-toolkit/internal/logger"
"errors"
"fmt"
"github.com/urfave/cli/v2"
"tags.cncf.io/container-device-interface/pkg/cdi"
)
type command struct {
logger logger.Interface
}
type config struct {
cdiSpecDirs cli.StringSlice
}
// NewCommand constructs a cdi list command with the specified logger
func NewCommand(logger logger.Interface) *cli.Command {
c := command{
logger: logger,
}
return c.build()
}
// build creates the CLI command
func (m command) build() *cli.Command {
cfg := config{}
// Create the command
c := cli.Command{
Name: "list",
Usage: "List the available CDI devices",
Before: func(c *cli.Context) error {
return m.validateFlags(c, &cfg)
},
Action: func(c *cli.Context) error {
return m.run(c, &cfg)
},
}
c.Flags = []cli.Flag{
&cli.StringSliceFlag{
Name: "spec-dir",
Usage: "specify the directories to scan for CDI specifications",
Value: cli.NewStringSlice(cdi.DefaultSpecDirs...),
Destination: &cfg.cdiSpecDirs,
},
}
return &c
}
func (m command) validateFlags(c *cli.Context, cfg *config) error {
if len(cfg.cdiSpecDirs.Value()) == 0 {
return errors.New("at least one CDI specification directory must be specified")
}
return nil
}
func (m command) run(c *cli.Context, cfg *config) error {
registry, err := cdi.NewCache(
cdi.WithAutoRefresh(false),
cdi.WithSpecDirs(cfg.cdiSpecDirs.Value()...),
)
if err != nil {
return fmt.Errorf("failed to create CDI cache: %v", err)
}
_ = registry.Refresh()
if errors := registry.GetErrors(); len(errors) > 0 {
m.logger.Warningf("The following registry errors were reported:")
for k, err := range errors {
m.logger.Warningf("%v: %v", k, err)
}
}
devices := registry.ListDevices()
m.logger.Infof("Found %d CDI devices", len(devices))
for _, device := range devices {
fmt.Printf("%s\n", device)
}
return nil
}
/**
# Copyright (c) 2024, HCUOpt CORPORATION. All rights reserved.
**/
package root
import (
"dtk-container-toolkit/internal/logger"
"dtk-container-toolkit/pkg/c3000cdi/spec"
transformroot "dtk-container-toolkit/pkg/c3000cdi/transform/root"
"fmt"
"io"
"os"
"github.com/urfave/cli/v2"
"tags.cncf.io/container-device-interface/pkg/cdi"
)
type command struct {
logger logger.Interface
}
type transformOptions struct {
input string
output string
}
type options struct {
transformOptions
from string
to string
relativeTo string
}
// NewCommand constructs a generate-cdi command with the specified logger
func NewCommand(logger logger.Interface) *cli.Command {
c := command{
logger: logger,
}
return c.build()
}
// build creates the CLI command
func (m command) build() *cli.Command {
opts := options{}
c := cli.Command{
Name: "root",
Usage: "Apply a root transform to a CDI specification",
Before: func(c *cli.Context) error {
return m.validateFlags(c, &opts)
},
Action: func(c *cli.Context) error {
return m.run(c, &opts)
},
}
c.Flags = []cli.Flag{
&cli.StringFlag{
Name: "from",
Usage: "specify the root to be transformed",
Destination: &opts.from,
},
&cli.StringFlag{
Name: "input",
Usage: "Specify the file to read the CDI specification from. If this is '-' the specification is read from STDIN",
Value: "-",
Destination: &opts.input,
},
&cli.StringFlag{
Name: "output",
Usage: "Specify the file to output the generated CDI specification to. If this is '' the specification is output to STDOUT",
Destination: &opts.output,
},
&cli.StringFlag{
Name: "relative-to",
Usage: "specify whether the transform is relative to the host or to the container. One of [ host | container ]",
Value: "host",
Destination: &opts.relativeTo,
},
&cli.StringFlag{
Name: "to",
Usage: "specify the replacement root. If this is the same as the from root, the transform is a no-op.",
Value: "",
Destination: &opts.to,
},
}
return &c
}
func (m command) validateFlags(c *cli.Context, opts *options) error {
switch opts.relativeTo {
case "host":
case "container":
default:
return fmt.Errorf("invalid --relative-to value: %v", opts.relativeTo)
}
return nil
}
func (m command) run(c *cli.Context, opts *options) error {
spec, err := opts.Load()
if err != nil {
return fmt.Errorf("failed to load CDI specification: %w", err)
}
err = transformroot.New(
transformroot.WithRoot(opts.from),
transformroot.WithTargetRoot(opts.to),
transformroot.WithRelativeTo(opts.relativeTo),
).Transform(spec.Raw())
if err != nil {
return fmt.Errorf("failed to transform CDI specification: %w", err)
}
return opts.Save(spec)
}
// Load lodas the input CDI specification
func (o transformOptions) Load() (spec.Interface, error) {
contents, err := o.getContents()
if err != nil {
return nil, fmt.Errorf("failed to read spec contents: %v", err)
}
raw, err := cdi.ParseSpec(contents)
if err != nil {
return nil, fmt.Errorf("failed to parse CDI spec: %v", err)
}
return spec.New(
spec.WithRawSpec(raw),
)
}
func (o transformOptions) getContents() ([]byte, error) {
if o.input == "-" {
return io.ReadAll(os.Stdin)
}
return os.ReadFile(o.input)
}
// Save saves the CDI specification to the output file
func (o transformOptions) Save(s spec.Interface) error {
if o.output == "" {
_, err := s.WriteTo(os.Stdout)
if err != nil {
return fmt.Errorf("failed to write CDI spec to STDOUT: %v", err)
}
return nil
}
return s.Save(o.output)
}
/**
# Copyright (c) 2024, HCUOpt CORPORATION. All rights reserved.
**/
package transform
import (
"dtk-container-toolkit/cmd/dtk-ctk/cdi/transform/root"
"dtk-container-toolkit/internal/logger"
"github.com/urfave/cli/v2"
)
type command struct {
logger logger.Interface
}
// NewCommand constructs a command with the specified logger
func NewCommand(logger logger.Interface) *cli.Command {
c := command{
logger: logger,
}
return c.build()
}
// build creates the CLI command
func (m command) build() *cli.Command {
c := cli.Command{
Name: "transform",
Usage: "Apply a transform to a CDI specification",
}
c.Flags = []cli.Flag{}
c.Subcommands = []*cli.Command{
root.NewCommand(m.logger),
}
return &c
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment