dubboctl/pkg/sdk/client.go (304 lines of code) (raw):

/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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 sdk import ( "bufio" "context" "fmt" "github.com/apache/dubbo-kubernetes/dubboctl/pkg/sdk/dubbo" "github.com/apache/dubbo-kubernetes/dubboctl/pkg/util" "github.com/pkg/errors" "github.com/spf13/cobra" "gopkg.in/yaml.v3" "os" "path/filepath" "strings" "time" ) type Client struct { templates *Templates repositories *Repositories repositoriesPath string builder Builder pusher Pusher deployer Deployer } type Builder interface { Build(context.Context, *dubbo.DubboConfig) error } type Pusher interface { Push(ctx context.Context, dc *dubbo.DubboConfig) (string, error) } type DeployOption func(f *DeployParams) type Deployer interface { Deploy(context.Context, *dubbo.DubboConfig, ...DeployOption) (DeploymentResult, error) } type DeploymentResult struct { Status Status Namespace string } type Status int const ( Failed Status = iota Deployed ) type DeployParams struct { skipBuiltCheck bool } type Option func(client *Client) func New(options ...Option) *Client { c := &Client{} for _, o := range options { o(c) } c.repositories = newRepositories(c) c.templates = newTemplates(c) return c } func (c *Client) Templates() *Templates { return c.templates } func (c *Client) Runtimes() ([]string, error) { runtimes := util.NewSortedSet() repos, err := c.Repositories().All() if err != nil { return []string{}, err } for _, repo := range repos { for _, runtime := range repo.Runtimes { runtimes.Add(runtime.Name) } } return runtimes.Items(), nil } func (c *Client) Repositories() *Repositories { return c.repositories } func (c *Client) Initialize(dcfg *dubbo.DubboConfig, initialized bool, cmd *cobra.Command) (*dubbo.DubboConfig, error) { var err error oldRoot := dcfg.Root dcfg.Root, err = filepath.Abs(dcfg.Root) if err != nil { return dcfg, err } if err = os.MkdirAll(dcfg.Root, 0o755); err != nil { return dcfg, err } has, err := hasInitialized(dcfg.Root) if err != nil { return dcfg, err } if has { return dcfg, fmt.Errorf("%v already initialized", dcfg.Root) } if dcfg.Root == "" { if dcfg.Root, err = os.Getwd(); err != nil { return dcfg, err } } if dcfg.Name == "" { dcfg.Name = nameFromPath(dcfg.Root) } if !initialized { if err := assertEmptyRoot(dcfg.Root); err != nil { return dcfg, err } } // TODO remove initiallized f := dubbo.NewDubboConfigWithTemplate(dcfg, initialized) if err = runDataDir(f.Root); err != nil { return f, err } if !initialized { err = c.Templates().Write(f) if err != nil { return f, err } } f.Created = time.Now() err = f.WriteFile() if err != nil { return f, err } err = f.WriteDockerfile(cmd) if err != nil { return f, err } return dubbo.NewDubboConfig(oldRoot) } type BuildOptions struct{} type BuildOption func(c *BuildOptions) func (c *Client) Build(ctx context.Context, dc *dubbo.DubboConfig, options ...BuildOption) (*dubbo.DubboConfig, error) { fmt.Println("Starting to built the image...") ctx, cancel := context.WithCancel(ctx) defer cancel() bo := BuildOptions{} for _, o := range options { o(&bo) } if err := c.builder.Build(ctx, dc); err != nil { return dc, err } if err := dc.Stamp(); err != nil { return dc, err } fmt.Printf("Image built completed: %v\n", dc.Image) return dc, nil } func (c *Client) Push(ctx context.Context, dc *dubbo.DubboConfig) (*dubbo.DubboConfig, error) { var err error if !dc.Built() { return dc, errors.New("not built") } if dc.ImageDigest, err = c.pusher.Push(ctx, dc); err != nil { return dc, err } return dc, nil } func (c *Client) Deploy(ctx context.Context, dc *dubbo.DubboConfig, opts ...DeployOption) (*dubbo.DubboConfig, error) { deployParams := &DeployParams{skipBuiltCheck: false} for _, opt := range opts { opt(deployParams) } go func() { <-ctx.Done() }() if dc.Name == "" { return dc, errors.New("name required") } result, err := c.deployer.Deploy(ctx, dc) if err != nil { fmt.Printf("deploy error: %v\n", err) return dc, err } dc.Deploy.Namespace = result.Namespace if result.Status == Deployed { // TODO } return dc, nil } func hasInitialized(path string) (bool, error) { var err error filename := filepath.Join(path, dubbo.LogFile) if _, err = os.Stat(filename); err != nil { if os.IsNotExist(err) { return false, nil } return false, err } bb, err := os.ReadFile(filename) if err != nil { return false, err } f := dubbo.DubboConfig{} if err = yaml.Unmarshal(bb, &f); err != nil { return false, err } return f.Initialized(), nil } func nameFromPath(path string) string { pathParts := strings.Split(strings.TrimRight(path, string(os.PathSeparator)), string(os.PathSeparator)) return pathParts[len(pathParts)-1] } func assertEmptyRoot(path string) (err error) { files, err := contentiousFilesIn(path) if err != nil { return } else if len(files) > 0 { return fmt.Errorf("the chosen directory '%v' contains contentious files: %v. Has the Service function already been created? Try either using a different directory, deleting the function if it exists, or manually removing the files", path, files) } empty, err := isEffectivelyEmpty(path) if err != nil { return } else if !empty { err = errors.New("the directory must be empty of visible files and recognized config files before it can be initialized") return } return } func runDataDir(root string) error { if err := os.MkdirAll(filepath.Join(root, dubbo.DataDir), os.ModePerm); err != nil { return err } filePath := filepath.Join(root, ".gitignore") roFile, err := os.Open(filePath) if err != nil && !os.IsNotExist(err) { return err } defer roFile.Close() if !os.IsNotExist(err) { s := bufio.NewScanner(roFile) for s.Scan() { if strings.HasPrefix(s.Text(), "# /"+dubbo.DataDir) { // if it was commented return nil // user wants it } if strings.HasPrefix(s.Text(), "#/"+dubbo.DataDir) { return nil // user wants it } if strings.HasPrefix(s.Text(), "/"+dubbo.DataDir) { // if it is there return nil // we're done } } } roFile.Close() rwFile, err := os.OpenFile(filePath, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0o644) if err != nil { return err } defer rwFile.Close() if _, err = rwFile.WriteString(` # Applications use the .dubbo directory for local runtime data which should # generally not be tracked in source control. To instruct the system to track # .dubbo in source control, comment the following line (prefix it with '# '). /.dubbo `); err != nil { return err } if err = rwFile.Sync(); err != nil { fmt.Fprintf(os.Stderr, "warning: error when syncing .gitignore. %s\n", err) } return nil } var contentiousFiles = []string{ dubbo.LogFile, ".gitignore", } func contentiousFilesIn(dir string) (contentious []string, err error) { files, err := os.ReadDir(dir) for _, file := range files { for _, name := range contentiousFiles { if file.Name() == name { contentious = append(contentious, name) } } } return } func isEffectivelyEmpty(dir string) (bool, error) { files, err := os.ReadDir(dir) if err != nil { return false, err } for _, file := range files { if !strings.HasPrefix(file.Name(), ".") { return false, nil } } return true, nil } func WithRepositoriesPath(path string) Option { return func(c *Client) { c.repositoriesPath = path } } func WithBuilder(b Builder) Option { return func(c *Client) { c.builder = b } } func WithPusher(pusher Pusher) Option { return func(c *Client) { c.pusher = pusher } } func WithDeployer(d Deployer) Option { return func(c *Client) { c.deployer = d } }