-
Notifications
You must be signed in to change notification settings - Fork 156
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement the tool registry in the piped side #5343
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
// Copyright 2024 The PipeCD Authors. | ||
// | ||
// 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 grpcapi | ||
|
||
import ( | ||
"bytes" | ||
"context" | ||
"fmt" | ||
"os" | ||
"os/exec" | ||
"path/filepath" | ||
"runtime" | ||
"text/template" | ||
|
||
"golang.org/x/sync/singleflight" | ||
) | ||
|
||
type toolRegistry struct { | ||
toolsDir string | ||
tmpDir string | ||
group singleflight.Group | ||
} | ||
|
||
type templateValues struct { | ||
Name string | ||
Version string | ||
OutPath string | ||
TmpDir string | ||
Arch string | ||
Os string | ||
} | ||
|
||
func newToolRegistry(toolsDir, tmpDir string) (*toolRegistry, error) { | ||
tmpDir, err := os.MkdirTemp(tmpDir, "tool-registry") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Ask] Is it enough just to create the tmp dir for the tool installation in the newToolRegistry. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you mean to call like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, I mean fixing as newToolRegistry(toolsDir string) and call os.MkdirTemp inside it as you said. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I got your point! That's enough. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fixed on this commit |
||
if err != nil { | ||
return nil, fmt.Errorf("failed to create a temporary directory: %w", err) | ||
} | ||
return &toolRegistry{ | ||
toolsDir: toolsDir, | ||
tmpDir: tmpDir, | ||
}, nil | ||
} | ||
|
||
func (r *toolRegistry) newTmpDir() (string, error) { | ||
return os.MkdirTemp(r.tmpDir, "") | ||
} | ||
|
||
func (r *toolRegistry) outPath() (string, error) { | ||
target, err := r.newTmpDir() | ||
if err != nil { | ||
return "", err | ||
} | ||
return filepath.Join(target, "out"), nil | ||
} | ||
|
||
func (r *toolRegistry) InstallTool(ctx context.Context, name, version, script string) (string, error) { | ||
out, err, _ := r.group.Do(fmt.Sprintf("%s-%s", name, version), func() (interface{}, error) { | ||
return r.installTool(ctx, name, version, script) | ||
}) | ||
if err != nil { | ||
return "", fmt.Errorf("failed to install the tool %s-%s: %w", name, version, err) | ||
} | ||
return out.(string), nil // the result is always string | ||
} | ||
|
||
func (r *toolRegistry) installTool(ctx context.Context, name, version, script string) (path string, err error) { | ||
outPath, err := r.outPath() | ||
if err != nil { | ||
return "", err | ||
} | ||
|
||
tmpDir, err := r.newTmpDir() | ||
if err != nil { | ||
return "", err | ||
} | ||
|
||
t, err := template.New("install script").Parse(script) | ||
if err != nil { | ||
return "", err | ||
} | ||
|
||
vars := templateValues{ | ||
Name: name, | ||
Version: version, | ||
OutPath: outPath, | ||
TmpDir: tmpDir, | ||
Arch: runtime.GOARCH, | ||
Os: runtime.GOOS, | ||
} | ||
var buf bytes.Buffer | ||
if err := t.Execute(&buf, vars); err != nil { | ||
return "", err | ||
} | ||
|
||
cmd := exec.CommandContext(ctx, "/bin/sh", "-c", buf.String()) | ||
if out, err := cmd.CombinedOutput(); err != nil { | ||
return "", fmt.Errorf("failed to execute the install script: %w, output: %s", err, string(out)) | ||
} | ||
|
||
if err := os.Chmod(outPath, 0o755); err != nil { | ||
return "", err | ||
} | ||
|
||
target := filepath.Join(r.toolsDir, fmt.Sprintf("%s-%s", name, version)) | ||
if out, err := exec.CommandContext(ctx, "/bin/sh", "-c", "mv "+outPath+" "+target).CombinedOutput(); err != nil { | ||
return "", fmt.Errorf("failed to move the installed binary: %w, output: %s", err, string(out)) | ||
} | ||
|
||
if err := os.RemoveAll(tmpDir); err != nil { | ||
return "", err | ||
} | ||
|
||
return target, nil | ||
} | ||
|
||
func (r *toolRegistry) Close() error { | ||
if err := os.RemoveAll(r.tmpDir); err != nil { | ||
return err | ||
} | ||
return nil | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
// Copyright 2024 The PipeCD Authors. | ||
// | ||
// 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 grpcapi | ||
|
||
import ( | ||
"context" | ||
"os" | ||
"strings" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func TestToolRegistry_InstallTool(t *testing.T) { | ||
t.Parallel() | ||
|
||
ctx := context.Background() | ||
|
||
toolsDir, err := os.MkdirTemp(t.TempDir(), "tools") | ||
require.NoError(t, err) | ||
tmpDir, err := os.MkdirTemp(t.TempDir(), "tmp") | ||
require.NoError(t, err) | ||
|
||
registry := &toolRegistry{ | ||
toolsDir: toolsDir, | ||
tmpDir: tmpDir, | ||
} | ||
|
||
tests := []struct { | ||
name string | ||
toolName string | ||
toolVersion string | ||
script string | ||
wantErr bool | ||
}{ | ||
{ | ||
name: "valid script", | ||
toolName: "tool-a", | ||
toolVersion: "1.0.0", | ||
script: `touch {{ .OutPath }}`, | ||
wantErr: false, | ||
}, | ||
{ | ||
name: "output is not found", | ||
toolName: "tool-b", | ||
toolVersion: "1.0.0", | ||
script: "exit 0", | ||
wantErr: true, | ||
}, | ||
{ | ||
name: "script failed", | ||
toolName: "tool-c", | ||
toolVersion: "1.0.0", | ||
script: "exit 1", | ||
wantErr: true, | ||
}, | ||
} | ||
|
||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
t.Parallel() | ||
|
||
out, err := registry.InstallTool(ctx, tt.toolName, tt.toolVersion, tt.script) | ||
if tt.wantErr { | ||
require.Error(t, err) | ||
return | ||
} | ||
require.NoError(t, err) | ||
assert.FileExists(t, out) | ||
assert.True(t, strings.HasSuffix(out, tt.toolName+"-"+tt.toolVersion), "output path should have the tool {name}-{version}, got %s", out) | ||
}) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since the tool is not plugin-specific, I think we should have some kind of cache to avoid redundant installation. If the name and version are the same, then we should return the installed path immediately instead of running the script, wdyt?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree. I'll add the cache like below.
pipecd/pkg/app/piped/toolregistry/registry.go
Lines 116 to 121 in 23eddac
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@khanhtc1202
fixed on this commit, thanks
75d7daa