diff --git a/.changeset/stupid-poems-glow.md b/.changeset/stupid-poems-glow.md new file mode 100644 index 00000000000..bdd8acc66d5 --- /dev/null +++ b/.changeset/stupid-poems-glow.md @@ -0,0 +1,5 @@ +--- +"chainlink": minor +--- + +#internal rework operator_ui installer diff --git a/.gitignore b/.gitignore index f17a6bf1430..4f07e40516c 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,7 @@ tools/clroot/db.sqlite3-wal *.iml debug.env *.txt +operator_ui/install # codeship *.aes @@ -102,4 +103,3 @@ override*.toml .venv/ ocr_soak_report.csv - diff --git a/GNUmakefile b/GNUmakefile index 48e15e39fb4..589e750289f 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -83,7 +83,7 @@ docker-plugins: .PHONY: operator-ui operator-ui: ## Fetch the frontend - go generate ./core/web + go run operator_ui/install.go . .PHONY: abigen abigen: ## Build & install abigen. diff --git a/core/web/middleware.go b/core/web/middleware.go index 17bd7e65eba..6e9378e618f 100644 --- a/core/web/middleware.go +++ b/core/web/middleware.go @@ -21,7 +21,7 @@ import ( // inside this module. To achieve this, we direct webpack to output all of the compiled assets // in this module's folder under the "assets" directory. -//go:generate ../../operator_ui/install.sh +//go:generate go run ../../operator_ui/install.go ../.. //go:embed "assets" var uiEmbedFs embed.FS diff --git a/operator_ui/README.md b/operator_ui/README.md index 07bda2cd1dc..ccb5173631a 100644 --- a/operator_ui/README.md +++ b/operator_ui/README.md @@ -12,7 +12,12 @@ This package is responsible for rendering the UI of the chainlink node, which al ### Requirements -The `install.sh` script handles installing the specified tag of operator UI within the [tag file](./TAG). When executed, it downloads then moves the static assets of operator UI into the `core/web/assets` path. Then, when the chainlink binary is built, these assets are included into the build that gets served. +The `install.go` script handles installing the specified tag of operator UI within the [tag file](./TAG). When executed, it downloads then moves the static assets of operator UI into the `core/web/assets` path. Then, when the chainlink binary is built, these assets are included into the build that gets served. + +```sh +# The argument is the path from the this directory to the root of the repository +go run ./install.go .. +``` ## Updates diff --git a/operator_ui/install.go b/operator_ui/install.go new file mode 100644 index 00000000000..1e09783db66 --- /dev/null +++ b/operator_ui/install.go @@ -0,0 +1,162 @@ +package main + +import ( + "archive/tar" + "compress/gzip" + "context" + "fmt" + "io" + "log" + "net/http" + "os" + "path" + "path/filepath" + "strings" + "time" +) + +func main() { + const ( + owner = "smartcontractkit" + repo = "operator-ui" + fullRepo = owner + "/" + repo + tagPath = "operator_ui/TAG" + unpackDir = "core/web/assets" + downloadTimeoutSeconds = 10 + ) + // Grab first argument as root directory + if len(os.Args) < 2 { + log.Fatalln("Usage: install.go ") + } + rootDir := os.Args[1] + + tag := mustReadTagFile(path.Join(rootDir, tagPath)) + strippedTag := stripVersionFromTag(tag) + assetName := fmt.Sprintf("%s-%s-%s.tgz", owner, repo, strippedTag) + downloadUrl := fmt.Sprintf("https://github.com/%s/releases/download/%s/%s", fullRepo, tag, assetName) + + // Assuming that we're in "root/operator_ui/" + unpackPath := filepath.Join(rootDir, unpackDir) + err := rmrf(unpackPath) + if err != nil { + log.Fatalln(err) + } + + subPath := "package/artifacts/" + mustDownloadSubAsset(downloadUrl, downloadTimeoutSeconds, unpackPath, subPath) +} + +func mustReadTagFile(file string) string { + tagBytes, err := os.ReadFile(file) + if err != nil { + log.Fatalln(err) + } + tag := string(tagBytes) + return strings.TrimSpace(tag) +} + +func stripVersionFromTag(tag string) string { + return strings.TrimPrefix(tag, "v") +} + +func rmrf(path string) error { + err := os.RemoveAll(path) + if err != nil { + return err + } + + err = os.Mkdir(path, 0755) + return err +} + +// Download a sub asset from a .tgz file and extract it to a destination path +func mustDownloadSubAsset(downloadUrl string, downloadTimeoutSeconds int, unpackPath string, subPath string) { + fmt.Println("Downloading", downloadUrl) + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(downloadTimeoutSeconds)*time.Second) + defer cancel() + /* #nosec G107 */ + req, err := http.NewRequestWithContext(ctx, http.MethodGet, downloadUrl, nil) + if err != nil { + log.Fatalln(err) + } + resp, err := http.DefaultClient.Do(req) + if err != nil { + log.Fatalln(err) + } + + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + log.Fatalln(fmt.Errorf("failed to fetch asset: %s", resp.Status)) + } + + err = decompressTgzSubpath(resp.Body, unpackPath, subPath) + if err != nil { + log.Fatalln(err) + } +} + +// Decompress a .tgz file to a destination path, only extracting files that are in the subpath +// +// Subpath files are extracted to the root of the destination path, rather than preserving the subpath +func decompressTgzSubpath(file io.Reader, destPath string, subPath string) error { + // Create a gzip reader + gzr, err := gzip.NewReader(file) + if err != nil { + return fmt.Errorf("failed to create gzip reader: %w", err) + } + defer gzr.Close() + + // Create a tar reader + tr := tar.NewReader(gzr) + + // Iterate through the files in the tar archive + for { + header, err := tr.Next() + switch { + case err == io.EOF: + return nil // End of tar archive + case err != nil: + return fmt.Errorf("failed to read tar file: %w", err) + case header == nil: + continue + } + // skip files that arent in the subpath + if !strings.HasPrefix(header.Name, subPath) { + continue + } + + // Strip the subpath from the header name + header.Name = strings.TrimPrefix(header.Name, subPath) + + // Target location where the dir/file should be created + target := fmt.Sprintf("%s/%s", destPath, header.Name) + + // Check the file type + switch header.Typeflag { + case tar.TypeDir: // Directory + if err := os.MkdirAll(target, 0755); err != nil { + return fmt.Errorf("failed to create directory: %w", err) + } + fmt.Println("Creating directory", target) + case tar.TypeReg: // Regular file + if err := writeFile(target, header, tr); err != nil { + return err + } + } + } +} + +func writeFile(target string, header *tar.Header, tr *tar.Reader) error { + /* #nosec G110 */ + f, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode)) + if err != nil { + return fmt.Errorf("failed to open file: %w", err) + } + defer f.Close() + + if _, err := io.Copy(f, tr); err != nil { + return fmt.Errorf("failed to write file: %w", err) + } + fmt.Println("Creating file", target) + return nil +} diff --git a/operator_ui/install.sh b/operator_ui/install.sh deleted file mode 100755 index f86c9a2f352..00000000000 --- a/operator_ui/install.sh +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env bash -set -e - -owner=smartcontractkit -repo=operator-ui -fullRepo=${owner}/${repo} -gitRoot="$(dirname -- "$0")/../" -cd "$gitRoot/operator_ui" -unpack_dir="$gitRoot/core/web/assets" -tag=$(cat TAG) -# Remove the version prefix "v" -strippedTag="${tag:1}" -# Taken from https://github.com/kennyp/asdf-golang/blob/master/lib/helpers.sh -msg() { - echo -e "\033[32m$1\033[39m" >&2 -} - -err() { - echo -e "\033[31m$1\033[39m" >&2 -} - -fail() { - err "$1" - exit 1 -} - -msg "Getting release $tag for $fullRepo" -# https://docs.github.com/en/rest/releases/releases#get-a-release-by-tag-name -asset_name=${owner}-${repo}-${strippedTag}.tgz -download_url=https://github.com/${fullRepo}/releases/download/${tag}/${asset_name} - -# Inspired from https://github.com/kennyp/asdf-golang/blob/master/bin/download#L29 -msg "Download URL: ${download_url}" -# Check if we're able to download first -http_code=$(curl -LIs -w '%{http_code}' -o /dev/null "$download_url") -if [ "$http_code" -eq 404 ] || [ "$http_code" -eq 403 ]; then - fail "URL: ${download_url} returned status ${http_code}" -fi -# Then go ahead if we get a success code -msg "Downloading ${fullRepo}:${tag} asset: $asset_name..." -msg "" -curl -L -o "$asset_name" "$download_url" - -msg "Unpacking asset $asset_name" -tar -xvzf "$asset_name" - -msg "" -msg "Removing old contents of $unpack_dir" -rm -rf "$unpack_dir" -msg "Copying contents of package/artifacts to $unpack_dir" -cp -rf package/artifacts/. "$unpack_dir" || true - -msg "Cleaning up" -rm -r package -rm "$asset_name"