dubboctl/pkg/sdk/repository.go (293 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 (
"errors"
"fmt"
"github.com/apache/dubbo-kubernetes/dubboctl/pkg/util"
"github.com/go-git/go-billy/v5/memfs"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/storage/memory"
"net/url"
"os"
"path"
"path/filepath"
"strings"
)
type Repository struct {
Name string
Runtimes []Runtime
fs util.Filesystem
uri string
}
type repositoryConfig struct {
DefaultName string `yaml:"name,omitempty"`
TemplatesPath string `yaml:"templates,omitempty"`
}
func NewRepository(name, uri string) (r Repository, err error) {
r = Repository{
uri: uri,
}
fs, err := filesystemFromURI(uri)
if err != nil {
return Repository{}, fmt.Errorf("failed to get repository from URI (%q): %w", uri, err)
}
r.fs = fs
repoConfig := repositoryConfig{}
if repoConfig.TemplatesPath != "" {
if err = checkDir(r.fs, repoConfig.TemplatesPath); err != nil {
err = fmt.Errorf("templates path '%v' does not exist in repo '%v'. %v",
repoConfig.TemplatesPath, r.Name, err)
return
}
} else {
repoConfig.TemplatesPath = "."
}
r.Name, err = repositoryDefaultName(repoConfig.DefaultName, uri)
if err != nil {
return
}
if name != "" {
r.Name = name
}
r.Runtimes, err = repositoryRuntimes(fs, r.Name, repoConfig)
return
}
func repositoryDefaultName(name, uri string) (string, error) {
if name != "" {
return name, nil
}
if uri != "" {
parsed, err := url.Parse(uri)
if err != nil {
return "", err
}
ss := strings.Split(parsed.Path, "/")
if len(ss) > 0 {
return strings.TrimSuffix(ss[len(ss)-1], ".git"), nil
}
}
return DefaultRepositoryName, nil
}
func repositoryRuntimes(fs util.Filesystem, repoName string, repoConfig repositoryConfig) (runtimes []Runtime, err error) {
runtimes = []Runtime{}
fis, err := fs.ReadDir(repoConfig.TemplatesPath)
if err != nil {
return
}
for _, fi := range fis {
if !fi.IsDir() || strings.HasPrefix(fi.Name(), ".") {
continue
}
runtime := Runtime{
Name: fi.Name(),
}
runtime.Templates, err = runtimeTemplates(fs, repoConfig.TemplatesPath, repoName, runtime.Name)
if err != nil {
return
}
runtimes = append(runtimes, runtime)
}
return
}
func runtimeTemplates(fs util.Filesystem, templatesPath, repoName, runtimeName string) (templates []Template, err error) {
runtimePath := path.Join(templatesPath, runtimeName)
if err = checkDir(fs, runtimePath); err != nil {
err = fmt.Errorf("runtime path '%v' not found. %v", runtimePath, err)
return
}
fis, err := fs.ReadDir(runtimePath)
if err != nil {
return
}
for _, fi := range fis {
if !fi.IsDir() || strings.HasPrefix(fi.Name(), ".") {
continue
}
t := template{
name: fi.Name(),
repository: repoName,
runtime: runtimeName,
fs: util.NewSubFS(path.Join(runtimePath, fi.Name()), fs),
}
templates = append(templates, t)
}
return
}
type Runtime struct {
Name string
Templates []Template
}
func (r *Repository) Template(runtimeName, name string) (t Template, err error) {
runtime, err := r.Runtime(runtimeName)
if err != nil {
return
}
for _, t := range runtime.Templates {
if t.Name() == name {
return t, nil
}
}
return nil, fmt.Errorf("template not found")
}
func (r *Repository) Templates(runtimeName string) ([]Template, error) {
for _, runtime := range r.Runtimes {
if runtime.Name == runtimeName {
return runtime.Templates, nil
}
}
return nil, nil
}
func (r *Repository) Runtime(name string) (runtime Runtime, err error) {
if name == "" {
return Runtime{}, fmt.Errorf("language runtime required")
}
for _, runtime = range r.Runtimes {
if runtime.Name == name {
return runtime, err
}
}
return Runtime{}, fmt.Errorf("language runtime not found")
}
func (r *Repository) Write(dest string) (err error) {
if r.fs == nil {
return errors.New("the write operation is not supported on this repo")
}
fs := r.fs
if _, ok := r.fs.(util.BillyFilesystem); ok {
var (
tempDir string
clone *git.Repository
wt *git.Worktree
)
if tempDir, err = os.MkdirTemp("", "dubbo"); err != nil {
return
}
if clone, err = git.PlainClone(tempDir, false,
getGitCloneOptions(r.uri)); err != nil {
return fmt.Errorf("failed to plain clone repository: %w", err)
}
if wt, err = clone.Worktree(); err != nil {
return fmt.Errorf("failed to get worktree: %w", err)
}
fs = util.NewBillyFilesystem(wt.Filesystem)
}
return util.CopyFromFS(".", dest, fs)
}
func (r *Repository) URL() string {
uri := r.uri
if uri == "" {
return ""
}
if strings.HasPrefix(uri, "file://") {
uri = filepath.FromSlash(r.uri[7:])
}
repo, err := git.PlainOpen(uri)
if err != nil {
return ""
}
c, err := repo.Config()
if err != nil {
return ""
}
ref, _ := repo.Head()
if _, ok := c.Remotes["origin"]; ok {
urls := c.Remotes["origin"].URLs
if len(urls) > 0 {
return urls[0] + "#" + ref.Name().Short()
}
}
return ""
}
func filesystemFromURI(uri string) (fs util.Filesystem, err error) {
if uri == "" {
return EmbeddedTemplatesFS, nil
}
if isNonBareGitRepo(uri) {
return filesystemFromPath(uri)
}
fs, err = FilesystemFromRepo(uri)
if fs != nil || err != nil {
return
}
return filesystemFromPath(uri)
}
func isNonBareGitRepo(uri string) bool {
parsed, err := url.Parse(uri)
if err != nil {
return false
}
if parsed.Scheme != "file" {
return false
}
p := filepath.Join(filepath.FromSlash(uri[7:]), ".git")
fi, err := os.Stat(p)
if err != nil {
return false
}
return fi.IsDir()
}
func filesystemFromPath(uri string) (fs util.Filesystem, err error) {
parsed, err := url.Parse(uri)
if err != nil {
return
}
if parsed.Scheme != "file" {
return nil, fmt.Errorf("only file scheme is supported")
}
path := filepath.FromSlash(uri[7:])
if _, err := os.Stat(path); os.IsNotExist(err) {
return nil, fmt.Errorf("path does not exist: %v", path)
}
return util.NewOsFilesystem(path), nil
}
func FilesystemFromRepo(uri string) (util.Filesystem, error) {
clone, err := git.Clone(memory.NewStorage(),
memfs.New(),
getGitCloneOptions(uri),
)
if err != nil {
if isRepoNotFoundError(err) {
return nil, nil
}
if isBranchNotFoundError(err) {
return nil, fmt.Errorf("failed to clone repository: branch not found for uri %s", uri)
}
return nil, fmt.Errorf("failed to clone repository: %w", err)
}
wt, err := clone.Worktree()
if err != nil {
return nil, err
}
return util.NewBillyFilesystem(wt.Filesystem), nil
}
func getGitCloneOptions(uri string) *git.CloneOptions {
branch := ""
splitUri := strings.Split(uri, "#")
if len(splitUri) > 1 {
uri = splitUri[0]
branch = splitUri[1]
}
opt := &git.CloneOptions{
URL: uri, Depth: 1, Tags: git.NoTags,
RecurseSubmodules: git.NoRecurseSubmodules,
}
if branch != "" {
opt.ReferenceName = plumbing.NewBranchReferenceName(branch)
}
return opt
}
func isRepoNotFoundError(err error) bool {
return err != nil && err.Error() == "repository not found"
}
func isBranchNotFoundError(err error) bool {
return err != nil && err.Error() == "reference not found"
}
func checkDir(fs util.Filesystem, path string) error {
fi, err := fs.Stat(path)
if err != nil && os.IsNotExist(err) {
err = fmt.Errorf("path '%v` not found", path)
} else if err == nil && !fi.IsDir() {
err = fmt.Errorf("path '%v' is not a directory", path)
}
return err
}