From 9fe6d76a30eccd1f9a09a602436caae0528b157d Mon Sep 17 00:00:00 2001 From: Mostafa Moradian Date: Tue, 5 Sep 2023 19:25:59 +0200 Subject: [PATCH 1/2] Refactor all log.Fatal calls to log.Panic to prevent issues with defer --- cmd/config_init.go | 2 +- cmd/config_lint.go | 2 +- cmd/plugin_init.go | 2 +- cmd/plugin_install.go | 492 +------------------------------------- cmd/plugin_lint.go | 2 +- cmd/plugin_list.go | 2 +- cmd/run.go | 4 +- cmd/utils.go | 492 ++++++++++++++++++++++++++++++++++++++ config/config.go | 30 +-- logging/logger.go | 4 +- logging/logger_windows.go | 4 +- tracing/tracing.go | 4 +- 12 files changed, 528 insertions(+), 512 deletions(-) diff --git a/cmd/config_init.go b/cmd/config_init.go index 8c8edd2a..9cc52db4 100644 --- a/cmd/config_init.go +++ b/cmd/config_init.go @@ -24,7 +24,7 @@ var configInitCmd = &cobra.Command{ AttachStacktrace: config.DefaultAttachStacktrace, }) if err != nil { - log.Fatal("Sentry initialization failed: ", err) + log.Panic("Sentry initialization failed: ", err) } // Flush buffered events before the program terminates. diff --git a/cmd/config_lint.go b/cmd/config_lint.go index bcd143e2..a4a18fb5 100644 --- a/cmd/config_lint.go +++ b/cmd/config_lint.go @@ -22,7 +22,7 @@ var configLintCmd = &cobra.Command{ AttachStacktrace: config.DefaultAttachStacktrace, }) if err != nil { - log.Fatal("Sentry initialization failed: ", err) + log.Panic("Sentry initialization failed: ", err) } // Flush buffered events before the program terminates. diff --git a/cmd/plugin_init.go b/cmd/plugin_init.go index 38eead32..8fdde99c 100644 --- a/cmd/plugin_init.go +++ b/cmd/plugin_init.go @@ -22,7 +22,7 @@ var pluginInitCmd = &cobra.Command{ AttachStacktrace: config.DefaultAttachStacktrace, }) if err != nil { - log.Fatal("Sentry initialization failed: ", err) + log.Panic("Sentry initialization failed: ", err) } // Flush buffered events before the program terminates. diff --git a/cmd/plugin_install.go b/cmd/plugin_install.go index d802ba67..2a0f45fe 100644 --- a/cmd/plugin_install.go +++ b/cmd/plugin_install.go @@ -1,27 +1,13 @@ package cmd import ( - "archive/tar" - "archive/zip" - "compress/gzip" - "context" - "errors" - "io" "log" - "net/http" "os" - "path" - "path/filepath" - "regexp" - "runtime" "strings" - "github.com/codingsince1985/checksum" "github.com/gatewayd-io/gatewayd/config" "github.com/getsentry/sentry-go" - "github.com/google/go-github/v53/github" "github.com/spf13/cobra" - "gopkg.in/yaml.v3" ) const ( @@ -43,7 +29,7 @@ var ( // pluginInstallCmd represents the plugin install command. var pluginInstallCmd = &cobra.Command{ Use: "install", - Short: "Install a plugin from a remote location", + Short: "Install a plugin from a local archive or a GitHub repository", Example: " gatewayd plugin install github.com/gatewayd-io/gatewayd-plugin-cache@latest", Run: func(cmd *cobra.Command, args []string) { // Enable Sentry. @@ -55,7 +41,7 @@ var pluginInstallCmd = &cobra.Command{ AttachStacktrace: config.DefaultAttachStacktrace, }) if err != nil { - log.Fatal("Sentry initialization failed: ", err) + log.Panic("Sentry initialization failed: ", err) } // Flush buffered events before the program terminates. @@ -66,482 +52,20 @@ var pluginInstallCmd = &cobra.Command{ // Validate the number of arguments. if len(args) < 1 { - log.Fatal( + log.Panic( "Invalid URL. Use the following format: github.com/account/repository@version") } - // Validate the URL. - validGitHubURL := regexp.MustCompile(GitHubURLRegex) - if !validGitHubURL.MatchString(args[0]) { - log.Fatal( - "Invalid URL. Use the following format: github.com/account/repository@version") - } - - // Get the plugin version. - pluginVersion := LatestVersion - splittedURL := strings.Split(args[0], "@") - // If the version is not specified, use the latest version. - if len(splittedURL) < NumParts { - log.Println("Version not specified. Using latest version") - } - if len(splittedURL) >= NumParts { - pluginVersion = splittedURL[1] - } - - // Get the plugin account and repository. - accountRepo := strings.Split(strings.TrimPrefix(splittedURL[0], GitHubURLPrefix), "/") - if len(accountRepo) != NumParts { - log.Fatal( - "Invalid URL. Use the following format: github.com/account/repository@version") - } - account := accountRepo[0] - pluginName := accountRepo[1] - if account == "" || pluginName == "" { - log.Fatal( - "Invalid URL. Use the following format: github.com/account/repository@version") - } - - // Get the release artifact from GitHub. - client := github.NewClient(nil) - var release *github.RepositoryRelease - var err error - if pluginVersion == LatestVersion || pluginVersion == "" { - // Get the latest release. - release, _, err = client.Repositories.GetLatestRelease( - context.Background(), account, pluginName) - } else if strings.HasPrefix(pluginVersion, "v") { - // Get an specific release. - release, _, err = client.Repositories.GetReleaseByTag( - context.Background(), account, pluginName, pluginVersion) - } - if err != nil { - log.Fatal("The plugin could not be found") - } - - if release == nil { - log.Fatal("The plugin could not be found") - } - - downloadFile := func(downloadURL string, releaseID int64, filename string) { - log.Println("Downloading", downloadURL) - - // Download the plugin. - readCloser, redirectURL, err := client.Repositories.DownloadReleaseAsset( - context.Background(), account, pluginName, releaseID, http.DefaultClient) - if err != nil { - log.Fatal("There was an error downloading the plugin: ", err) - } - - var reader io.ReadCloser - if readCloser != nil { - reader = readCloser - defer readCloser.Close() - } else if redirectURL != "" { - // Download the plugin from the redirect URL. - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - req, err := http.NewRequestWithContext(ctx, http.MethodGet, redirectURL, nil) - if err != nil { - log.Fatal("There was an error downloading the plugin: ", err) - } - - resp, err := http.DefaultClient.Do(req) - if err != nil { - log.Fatal("There was an error downloading the plugin: ", err) - } - defer resp.Body.Close() - - reader = resp.Body - } - - if reader != nil { - defer reader.Close() - } else { - log.Fatal("The plugin could not be downloaded, please try again later") - } - - // Create the output file in the current directory and write the downloaded content. - cwd, err := os.Getwd() - if err != nil { - log.Fatal("There was an error downloading the plugin: ", err) - } - output, err := os.Create(path.Join([]string{cwd, filename}...)) - if err != nil { - log.Fatal("There was an error downloading the plugin: ", err) - } - defer output.Close() - - // Write the bytes to the file. - _, err = io.Copy(output, reader) - if err != nil { - log.Fatal("There was an error downloading the plugin: ", err) - } - - log.Println("Download completed successfully") - } - - findAsset := func(match func(string) bool) (string, string, int64) { - // Find the matching release. - for _, asset := range release.Assets { - if match(asset.GetName()) { - return asset.GetName(), asset.GetBrowserDownloadURL(), asset.GetID() - } - } - return "", "", 0 - } - - // Get the archive extension. - archiveExt := ExtOthers - if runtime.GOOS == "windows" { - archiveExt = ExtWindows - } - - // Find and download the plugin binary from the release assets. - pluginFilename, downloadURL, releaseID := findAsset(func(name string) bool { - return strings.Contains(name, runtime.GOOS) && - strings.Contains(name, runtime.GOARCH) && - strings.Contains(name, archiveExt) - }) - if downloadURL != "" && releaseID != 0 { - downloadFile(downloadURL, releaseID, pluginFilename) - } else { - log.Fatal("The plugin file could not be found in the release assets") - } - - // Find and download the checksums.txt from the release assets. - checksumsFilename, downloadURL, releaseID := findAsset(func(name string) bool { - return strings.Contains(name, "checksums.txt") - }) - if checksumsFilename != "" && downloadURL != "" && releaseID != 0 { - downloadFile(downloadURL, releaseID, checksumsFilename) - } else { - log.Fatal("The checksum file could not be found in the release assets") - } - - // Read the checksums text file. - checksums, err := os.ReadFile(checksumsFilename) - if err != nil { - log.Fatal("There was an error reading the checksums file: ", err) - } - - // Get the checksum for the plugin binary. - sum, err := checksum.SHA256sum(pluginFilename) - if err != nil { - log.Fatal("There was an error calculating the checksum: ", err) - } - - // Verify the checksums. - checksumLines := strings.Split(string(checksums), "\n") - for _, line := range checksumLines { - if strings.Contains(line, pluginFilename) { - checksum := strings.Split(line, " ")[0] - if checksum != sum { - log.Fatal("Checksum verification failed") - } - - log.Println("Checksum verification passed") - break - } - } - - if pullOnly { - log.Println("Plugin binary downloaded to", pluginFilename) - return - } - - // Extract the archive. - var filenames []string - if runtime.GOOS == "windows" { - filenames = extractZip(pluginFilename, pluginOutputDir) + if strings.HasPrefix(args[0], GitHubURLPrefix) { + // Pull the plugin from GitHub. + installFromGitHub(cmd, args, pluginOutputDir, pullOnly) } else { - filenames = extractTarGz(pluginFilename, pluginOutputDir) + // Pull the plugin from a local archive. + log.Panic("Local archives are not supported yet") } - - // Find the extracted plugin binary. - localPath := "" - pluginFileSum := "" - for _, filename := range filenames { - if strings.Contains(filename, pluginName) { - log.Println("Plugin binary extracted to", filename) - localPath = filename - // Get the checksum for the extracted plugin binary. - // TODO: Should we verify the checksum using the checksum.txt file instead? - pluginFileSum, err = checksum.SHA256sum(filename) - if err != nil { - log.Fatal("There was an error calculating the checksum: ", err) - } - break - } - } - - // Remove the tar.gz file. - err = os.Remove(pluginFilename) - if err != nil { - log.Fatal("There was an error removing the downloaded plugin file: ", err) - } - - // Remove the checksums.txt file. - err = os.Remove(checksumsFilename) - if err != nil { - log.Fatal("There was an error removing the checksums file: ", err) - } - - // Create a new gatewayd_plugins.yaml file if it doesn't exist. - if _, err := os.Stat(pluginConfigFile); os.IsNotExist(err) { - generateConfig(cmd, Plugins, pluginConfigFile, false) - } - - // Read the gatewayd_plugins.yaml file. - pluginsConfig, err := os.ReadFile(pluginConfigFile) - if err != nil { - log.Fatal(err) - } - - // Get the registered plugins from the plugins configuration file. - var localPluginsConfig map[string]interface{} - if err := yaml.Unmarshal(pluginsConfig, &localPluginsConfig); err != nil { - log.Fatal("Failed to unmarshal the plugins configuration file: ", err) - } - pluginsList, ok := localPluginsConfig["plugins"].([]interface{}) //nolint:varnamelen - if !ok { - log.Fatal("There was an error reading the plugins file from disk") - } - - // Get the list of files in the repository. - var repoContents *github.RepositoryContent - repoContents, _, _, err = client.Repositories.GetContents( - context.Background(), account, pluginName, DefaultPluginConfigFilename, nil) - if err != nil { - log.Fatal("There was an error getting the default plugins configuration file: ", err) - } - // Get the contents of the file. - contents, err := repoContents.GetContent() - if err != nil { - log.Fatal("There was an error getting the default plugins configuration file: ", err) - } - - // Get the plugin configuration from the downloaded plugins configuration file. - var downloadedPluginConfig map[string]interface{} - if err := yaml.Unmarshal([]byte(contents), &downloadedPluginConfig); err != nil { - log.Fatal("Failed to unmarshal the downloaded plugins configuration file: ", err) - } - defaultPluginConfig, ok := downloadedPluginConfig["plugins"].([]interface{}) - if !ok { - log.Fatal("There was an error reading the plugins file from the repository") - } - // Get the plugin configuration. - pluginConfig, ok := defaultPluginConfig[0].(map[string]interface{}) - if !ok { - log.Fatal("There was an error reading the default plugin configuration") - } - - // Update the plugin's local path and checksum. - pluginConfig["localPath"] = localPath - pluginConfig["checksum"] = pluginFileSum - - // TODO: Check if the plugin is already installed. - - // Add the plugin config to the list of plugin configs. - pluginsList = append(pluginsList, pluginConfig) - // Merge the result back into the config map. - localPluginsConfig["plugins"] = pluginsList - - // Marshal the map into YAML. - updatedPlugins, err := yaml.Marshal(localPluginsConfig) - if err != nil { - log.Fatal("There was an error marshalling the plugins configuration: ", err) - } - - // Write the YAML to the plugins config file. - if err = os.WriteFile(pluginConfigFile, updatedPlugins, FilePermissions); err != nil { - log.Fatal("There was an error writing the plugins configuration file: ", err) - } - - // TODO: Clean up the plugin files if the installation fails. - // TODO: Add a rollback mechanism. - log.Println("Plugin installed successfully") }, } -func extractZip(filename, dest string) []string { - // Open and extract the zip file. - zipRc, err := zip.OpenReader(filename) - if err != nil { - if zipRc != nil { - zipRc.Close() - } - log.Fatal("There was an error opening the downloaded plugin file: ", err) - } - - // Create the output directory if it doesn't exist. - if err := os.MkdirAll(dest, FolderPermissions); err != nil { - log.Fatal("Failed to create directories: ", err) - } - - // Extract the files. - filenames := []string{} - for _, file := range zipRc.File { - switch fileInfo := file.FileInfo(); { - case fileInfo.IsDir(): - // Sanitize the path. - filename := filepath.Clean(file.Name) - if !path.IsAbs(filename) { - destPath := path.Join(dest, filename) - // Create the directory. - - if err := os.MkdirAll(destPath, FolderPermissions); err != nil { - log.Fatal("Failed to create directories: ", err) - } - } - case fileInfo.Mode().IsRegular(): - // Sanitize the path. - outFilename := filepath.Join(filepath.Clean(dest), filepath.Clean(file.Name)) - - // Check for ZipSlip. - if strings.HasPrefix(outFilename, string(os.PathSeparator)) { - log.Fatal("Invalid file path in zip archive, aborting") - } - - // Create the file. - outFile, err := os.Create(outFilename) - if err != nil { - log.Fatal("Failed to create file: ", err) - } - - // Open the file in the zip archive. - fileRc, err := file.Open() - if err != nil { - log.Fatal("Failed to open file in zip archive: ", err) - } - - // Copy the file contents. - if _, err := io.Copy(outFile, io.LimitReader(fileRc, MaxFileSize)); err != nil { - outFile.Close() - os.Remove(outFilename) - log.Fatal("Failed to write to the file: ", err) - } - outFile.Close() - - fileMode := file.FileInfo().Mode() - // Set the file permissions. - if fileMode.IsRegular() && fileMode&ExecFileMask != 0 { - if err := os.Chmod(outFilename, ExecFilePermissions); err != nil { - log.Fatal("Failed to set executable file permissions: ", err) - } - } else { - if err := os.Chmod(outFilename, FilePermissions); err != nil { - log.Fatal("Failed to set file permissions: ", err) - } - } - - filenames = append(filenames, outFile.Name()) - default: - log.Fatalf("Failed to extract zip archive: unknown type: %s", file.Name) - } - } - - if zipRc != nil { - zipRc.Close() - } - - return filenames -} - -func extractTarGz(filename, dest string) []string { - // Open and extract the tar.gz file. - gzipStream, err := os.Open(filename) - if err != nil { - log.Fatal("There was an error opening the downloaded plugin file: ", err) - } - - uncompressedStream, err := gzip.NewReader(gzipStream) - if err != nil { - if gzipStream != nil { - gzipStream.Close() - } - log.Fatal("Failed to extract tarball: ", err) - } - - // Create the output directory if it doesn't exist. - if err := os.MkdirAll(dest, FolderPermissions); err != nil { - log.Fatal("Failed to create directories: ", err) - } - - tarReader := tar.NewReader(uncompressedStream) - filenames := []string{} - - for { - header, err := tarReader.Next() - - if errors.Is(err, io.EOF) { - break - } - - if err != nil { - log.Fatal("Failed to extract tarball: ", err) - } - - switch header.Typeflag { - case tar.TypeDir: - // Sanitize the path - cleanPath := filepath.Clean(header.Name) - // Ensure it is not an absolute path - if !path.IsAbs(cleanPath) { - destPath := path.Join(dest, cleanPath) - if err := os.MkdirAll(destPath, FolderPermissions); err != nil { - log.Fatal("Failed to create directories: ", err) - } - } - case tar.TypeReg: - // Sanitize the path - outFilename := path.Join(filepath.Clean(dest), filepath.Clean(header.Name)) - - // Check for TarSlip. - if strings.HasPrefix(outFilename, string(os.PathSeparator)) { - log.Fatal("Invalid file path in tarball, aborting") - } - - // Create the file. - outFile, err := os.Create(outFilename) - if err != nil { - log.Fatal("Failed to create file: ", err) - } - if _, err := io.Copy(outFile, io.LimitReader(tarReader, MaxFileSize)); err != nil { - outFile.Close() - os.Remove(outFilename) - log.Fatal("Failed to write to the file: ", err) - } - outFile.Close() - - fileMode := header.FileInfo().Mode() - // Set the file permissions - if fileMode.IsRegular() && fileMode&ExecFileMask != 0 { - if err := os.Chmod(outFilename, ExecFilePermissions); err != nil { - log.Fatal("Failed to set executable file permissions: ", err) - } - } else { - if err := os.Chmod(outFilename, FilePermissions); err != nil { - log.Fatal("Failed to set file permissions: ", err) - } - } - - filenames = append(filenames, outFile.Name()) - default: - log.Fatalf( - "Failed to extract tarball: unknown type: %s in %s", - string(header.Typeflag), - header.Name) - } - } - - if gzipStream != nil { - gzipStream.Close() - } - - return filenames -} - func init() { pluginCmd.AddCommand(pluginInstallCmd) diff --git a/cmd/plugin_lint.go b/cmd/plugin_lint.go index 3d9acbe0..d86b968b 100644 --- a/cmd/plugin_lint.go +++ b/cmd/plugin_lint.go @@ -22,7 +22,7 @@ var pluginLintCmd = &cobra.Command{ AttachStacktrace: config.DefaultAttachStacktrace, }) if err != nil { - log.Fatal("Sentry initialization failed: ", err) + log.Panic("Sentry initialization failed: ", err) } // Flush buffered events before the program terminates. diff --git a/cmd/plugin_list.go b/cmd/plugin_list.go index affb26b2..1b9c7c86 100644 --- a/cmd/plugin_list.go +++ b/cmd/plugin_list.go @@ -24,7 +24,7 @@ var pluginListCmd = &cobra.Command{ AttachStacktrace: config.DefaultAttachStacktrace, }) if err != nil { - log.Fatal("Sentry initialization failed: ", err) + log.Panic("Sentry initialization failed: ", err) } // Flush buffered events before the program terminates. diff --git a/cmd/run.go b/cmd/run.go index 17902c2b..188b19d5 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -73,7 +73,7 @@ var runCmd = &cobra.Command{ shutdown := tracing.OTLPTracer(true, collectorURL, config.TracerName) defer func() { if err := shutdown(context.Background()); err != nil { - log.Fatal(err) + log.Panic(err) } }() } @@ -94,7 +94,7 @@ var runCmd = &cobra.Command{ }) if err != nil { span.RecordError(err) - log.Fatal("Sentry initialization failed: ", err) + log.Panic("Sentry initialization failed: ", err) } // Flush buffered events before the program terminates. diff --git a/cmd/utils.go b/cmd/utils.go index e3b3640b..83f2d8d4 100644 --- a/cmd/utils.go +++ b/cmd/utils.go @@ -1,19 +1,32 @@ package cmd import ( + "archive/tar" + "archive/zip" + "compress/gzip" "context" "encoding/json" + "errors" + "io" "log" + "net/http" "os" + "path" + "path/filepath" + "regexp" + "runtime" "strings" + "github.com/codingsince1985/checksum" "github.com/gatewayd-io/gatewayd/config" + "github.com/google/go-github/v53/github" jsonSchemaGenerator "github.com/invopop/jsonschema" "github.com/knadh/koanf" koanfJson "github.com/knadh/koanf/parsers/json" "github.com/knadh/koanf/parsers/yaml" jsonSchemaV5 "github.com/santhosh-tekuri/jsonschema/v5" "github.com/spf13/cobra" + yamlv3 "gopkg.in/yaml.v3" ) type ( @@ -185,3 +198,482 @@ func listPlugins(cmd *cobra.Command, pluginConfigFile string, onlyEnabled bool) logger.Printf(" Checksum: %s\n", plugin.Checksum) } } + +func extractZip(filename, dest string) []string { + // Open and extract the zip file. + zipRc, err := zip.OpenReader(filename) + if err != nil { + if zipRc != nil { + zipRc.Close() + } + log.Panic("There was an error opening the downloaded plugin file: ", err) + } + + // Create the output directory if it doesn't exist. + if err := os.MkdirAll(dest, FolderPermissions); err != nil { + log.Panic("Failed to create directories: ", err) + } + + // Extract the files. + filenames := []string{} + for _, file := range zipRc.File { + switch fileInfo := file.FileInfo(); { + case fileInfo.IsDir(): + // Sanitize the path. + filename := filepath.Clean(file.Name) + if !path.IsAbs(filename) { + destPath := path.Join(dest, filename) + // Create the directory. + + if err := os.MkdirAll(destPath, FolderPermissions); err != nil { + log.Panic("Failed to create directories: ", err) + } + } + case fileInfo.Mode().IsRegular(): + // Sanitize the path. + outFilename := filepath.Join(filepath.Clean(dest), filepath.Clean(file.Name)) + + // Check for ZipSlip. + if strings.HasPrefix(outFilename, string(os.PathSeparator)) { + log.Panic("Invalid file path in zip archive, aborting") + } + + // Create the file. + outFile, err := os.Create(outFilename) + if err != nil { + log.Panic("Failed to create file: ", err) + } + + // Open the file in the zip archive. + fileRc, err := file.Open() + if err != nil { + log.Panic("Failed to open file in zip archive: ", err) + } + + // Copy the file contents. + if _, err := io.Copy(outFile, io.LimitReader(fileRc, MaxFileSize)); err != nil { + outFile.Close() + os.Remove(outFilename) + log.Panic("Failed to write to the file: ", err) + } + outFile.Close() + + fileMode := file.FileInfo().Mode() + // Set the file permissions. + if fileMode.IsRegular() && fileMode&ExecFileMask != 0 { + if err := os.Chmod(outFilename, ExecFilePermissions); err != nil { + log.Panic("Failed to set executable file permissions: ", err) + } + } else { + if err := os.Chmod(outFilename, FilePermissions); err != nil { + log.Panic("Failed to set file permissions: ", err) + } + } + + filenames = append(filenames, outFile.Name()) + default: + log.Panicf("Failed to extract zip archive: unknown type: %s", file.Name) + } + } + + if zipRc != nil { + zipRc.Close() + } + + return filenames +} + +func extractTarGz(filename, dest string) []string { + // Open and extract the tar.gz file. + gzipStream, err := os.Open(filename) + if err != nil { + log.Panic("There was an error opening the downloaded plugin file: ", err) + } + + uncompressedStream, err := gzip.NewReader(gzipStream) + if err != nil { + if gzipStream != nil { + gzipStream.Close() + } + log.Panic("Failed to extract tarball: ", err) + } + + // Create the output directory if it doesn't exist. + if err := os.MkdirAll(dest, FolderPermissions); err != nil { + log.Panic("Failed to create directories: ", err) + } + + tarReader := tar.NewReader(uncompressedStream) + filenames := []string{} + + for { + header, err := tarReader.Next() + + if errors.Is(err, io.EOF) { + break + } + + if err != nil { + log.Panic("Failed to extract tarball: ", err) + } + + switch header.Typeflag { + case tar.TypeDir: + // Sanitize the path + cleanPath := filepath.Clean(header.Name) + // Ensure it is not an absolute path + if !path.IsAbs(cleanPath) { + destPath := path.Join(dest, cleanPath) + if err := os.MkdirAll(destPath, FolderPermissions); err != nil { + log.Panic("Failed to create directories: ", err) + } + } + case tar.TypeReg: + // Sanitize the path + outFilename := path.Join(filepath.Clean(dest), filepath.Clean(header.Name)) + + // Check for TarSlip. + if strings.HasPrefix(outFilename, string(os.PathSeparator)) { + log.Panic("Invalid file path in tarball, aborting") + } + + // Create the file. + outFile, err := os.Create(outFilename) + if err != nil { + log.Panic("Failed to create file: ", err) + } + if _, err := io.Copy(outFile, io.LimitReader(tarReader, MaxFileSize)); err != nil { + outFile.Close() + os.Remove(outFilename) + log.Panic("Failed to write to the file: ", err) + } + outFile.Close() + + fileMode := header.FileInfo().Mode() + // Set the file permissions + if fileMode.IsRegular() && fileMode&ExecFileMask != 0 { + if err := os.Chmod(outFilename, ExecFilePermissions); err != nil { + log.Panic("Failed to set executable file permissions: ", err) + } + } else { + if err := os.Chmod(outFilename, FilePermissions); err != nil { + log.Panic("Failed to set file permissions: ", err) + } + } + + filenames = append(filenames, outFile.Name()) + default: + log.Panicf( + "Failed to extract tarball: unknown type: %s in %s", + string(header.Typeflag), + header.Name) + } + } + + if gzipStream != nil { + gzipStream.Close() + } + + return filenames +} + +func findAsset(release *github.RepositoryRelease, match func(string) bool) (string, string, int64) { + if release == nil { + return "", "", 0 + } + + // Find the matching release. + for _, asset := range release.Assets { + if match(asset.GetName()) { + return asset.GetName(), asset.GetBrowserDownloadURL(), asset.GetID() + } + } + return "", "", 0 +} + +func downloadFile( + client *github.Client, account, pluginName, downloadURL string, + releaseID int64, filename string, +) { + log.Println("Downloading", downloadURL) + + // Download the plugin. + readCloser, redirectURL, err := client.Repositories.DownloadReleaseAsset( + context.Background(), account, pluginName, releaseID, http.DefaultClient) + if err != nil { + log.Panic("There was an error downloading the plugin: ", err) + } + + var reader io.ReadCloser + if readCloser != nil { + reader = readCloser + defer readCloser.Close() + } else if redirectURL != "" { + // Download the plugin from the redirect URL. + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, redirectURL, nil) + if err != nil { + log.Panic("There was an error downloading the plugin: ", err) + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + log.Panic("There was an error downloading the plugin: ", err) + } + defer resp.Body.Close() + + reader = resp.Body + } + + if reader != nil { + defer reader.Close() + } else { + log.Panic("The plugin could not be downloaded, please try again later") + } + + // Create the output file in the current directory and write the downloaded content. + cwd, err := os.Getwd() + if err != nil { + log.Panic("There was an error downloading the plugin: ", err) + } + output, err := os.Create(path.Join([]string{cwd, filename}...)) + if err != nil { + log.Panic("There was an error downloading the plugin: ", err) + } + defer output.Close() + + // Write the bytes to the file. + _, err = io.Copy(output, reader) + if err != nil { + log.Panic("There was an error downloading the plugin: ", err) + } + + log.Println("Download completed successfully") +} + +func installFromGitHub(cmd *cobra.Command, args []string, pluginOutputDir string, pullOnly bool) { + // Validate the URL. + validGitHubURL := regexp.MustCompile(GitHubURLRegex) + if !validGitHubURL.MatchString(args[0]) { + log.Panic( + "Invalid URL. Use the following format: github.com/account/repository@version") + } + + // Get the plugin version. + pluginVersion := LatestVersion + splittedURL := strings.Split(args[0], "@") + // If the version is not specified, use the latest version. + if len(splittedURL) < NumParts { + log.Println("Version not specified. Using latest version") + } + if len(splittedURL) >= NumParts { + pluginVersion = splittedURL[1] + } + + // Get the plugin account and repository. + accountRepo := strings.Split(strings.TrimPrefix(splittedURL[0], GitHubURLPrefix), "/") + if len(accountRepo) != NumParts { + log.Panic( + "Invalid URL. Use the following format: github.com/account/repository@version") + } + account := accountRepo[0] + pluginName := accountRepo[1] + if account == "" || pluginName == "" { + log.Panic( + "Invalid URL. Use the following format: github.com/account/repository@version") + } + + // Get the release artifact from GitHub. + client := github.NewClient(nil) + var release *github.RepositoryRelease + var err error + if pluginVersion == LatestVersion || pluginVersion == "" { + // Get the latest release. + release, _, err = client.Repositories.GetLatestRelease( + context.Background(), account, pluginName) + } else if strings.HasPrefix(pluginVersion, "v") { + // Get an specific release. + release, _, err = client.Repositories.GetReleaseByTag( + context.Background(), account, pluginName, pluginVersion) + } + if err != nil { + log.Panic("The plugin could not be found") + } + + if release == nil { + log.Panic("The plugin could not be found") + } + + // Get the archive extension. + archiveExt := ExtOthers + if runtime.GOOS == "windows" { + archiveExt = ExtWindows + } + + // Find and download the plugin binary from the release assets. + pluginFilename, downloadURL, releaseID := findAsset(release, func(name string) bool { + return strings.Contains(name, runtime.GOOS) && + strings.Contains(name, runtime.GOARCH) && + strings.Contains(name, archiveExt) + }) + if downloadURL != "" && releaseID != 0 { + downloadFile(client, account, pluginName, downloadURL, releaseID, pluginFilename) + } else { + log.Panic("The plugin file could not be found in the release assets") + } + + // Find and download the checksums.txt from the release assets. + checksumsFilename, downloadURL, releaseID := findAsset(release, func(name string) bool { + return strings.Contains(name, "checksums.txt") + }) + if checksumsFilename != "" && downloadURL != "" && releaseID != 0 { + downloadFile(client, account, pluginName, downloadURL, releaseID, checksumsFilename) + } else { + log.Panic("The checksum file could not be found in the release assets") + } + + // Read the checksums text file. + checksums, err := os.ReadFile(checksumsFilename) + if err != nil { + log.Panic("There was an error reading the checksums file: ", err) + } + + // Get the checksum for the plugin binary. + sum, err := checksum.SHA256sum(pluginFilename) + if err != nil { + log.Panic("There was an error calculating the checksum: ", err) + } + + // Verify the checksums. + checksumLines := strings.Split(string(checksums), "\n") + for _, line := range checksumLines { + if strings.Contains(line, pluginFilename) { + checksum := strings.Split(line, " ")[0] + if checksum != sum { + log.Panic("Checksum verification failed") + } + + log.Println("Checksum verification passed") + break + } + } + + if pullOnly { + log.Println("Plugin binary downloaded to", pluginFilename) + return + } + + // Extract the archive. + var filenames []string + if runtime.GOOS == "windows" { + filenames = extractZip(pluginFilename, pluginOutputDir) + } else { + filenames = extractTarGz(pluginFilename, pluginOutputDir) + } + + // Find the extracted plugin binary. + localPath := "" + pluginFileSum := "" + for _, filename := range filenames { + if strings.Contains(filename, pluginName) { + log.Println("Plugin binary extracted to", filename) + localPath = filename + // Get the checksum for the extracted plugin binary. + // TODO: Should we verify the checksum using the checksum.txt file instead? + pluginFileSum, err = checksum.SHA256sum(filename) + if err != nil { + log.Panic("There was an error calculating the checksum: ", err) + } + break + } + } + + // Remove the tar.gz file. + err = os.Remove(pluginFilename) + if err != nil { + log.Panic("There was an error removing the downloaded plugin file: ", err) + } + + // Remove the checksums.txt file. + err = os.Remove(checksumsFilename) + if err != nil { + log.Panic("There was an error removing the checksums file: ", err) + } + + // Create a new gatewayd_plugins.yaml file if it doesn't exist. + if _, err := os.Stat(pluginConfigFile); os.IsNotExist(err) { + generateConfig(cmd, Plugins, pluginConfigFile, false) + } + + // Read the gatewayd_plugins.yaml file. + pluginsConfig, err := os.ReadFile(pluginConfigFile) + if err != nil { + log.Panic(err) + } + + // Get the registered plugins from the plugins configuration file. + var localPluginsConfig map[string]interface{} + if err := yamlv3.Unmarshal(pluginsConfig, &localPluginsConfig); err != nil { + log.Panic("Failed to unmarshal the plugins configuration file: ", err) + } + pluginsList, ok := localPluginsConfig["plugins"].([]interface{}) //nolint:varnamelen + if !ok { + log.Panic("There was an error reading the plugins file from disk") + } + + // Get the list of files in the repository. + var repoContents *github.RepositoryContent + repoContents, _, _, err = client.Repositories.GetContents( + context.Background(), account, pluginName, DefaultPluginConfigFilename, nil) + if err != nil { + log.Panic("There was an error getting the default plugins configuration file: ", err) + } + // Get the contents of the file. + contents, err := repoContents.GetContent() + if err != nil { + log.Panic("There was an error getting the default plugins configuration file: ", err) + } + + // Get the plugin configuration from the downloaded plugins configuration file. + var downloadedPluginConfig map[string]interface{} + if err := yamlv3.Unmarshal([]byte(contents), &downloadedPluginConfig); err != nil { + log.Panic("Failed to unmarshal the downloaded plugins configuration file: ", err) + } + defaultPluginConfig, ok := downloadedPluginConfig["plugins"].([]interface{}) + if !ok { + log.Panic("There was an error reading the plugins file from the repository") + } + // Get the plugin configuration. + pluginConfig, ok := defaultPluginConfig[0].(map[string]interface{}) + if !ok { + log.Panic("There was an error reading the default plugin configuration") + } + + // Update the plugin's local path and checksum. + pluginConfig["localPath"] = localPath + pluginConfig["checksum"] = pluginFileSum + + // TODO: Check if the plugin is already installed. + + // Add the plugin config to the list of plugin configs. + pluginsList = append(pluginsList, pluginConfig) + // Merge the result back into the config map. + localPluginsConfig["plugins"] = pluginsList + + // Marshal the map into YAML. + updatedPlugins, err := yamlv3.Marshal(localPluginsConfig) + if err != nil { + log.Panic("There was an error marshalling the plugins configuration: ", err) + } + + // Write the YAML to the plugins config file. + if err = os.WriteFile(pluginConfigFile, updatedPlugins, FilePermissions); err != nil { + log.Panic("There was an error writing the plugins configuration file: ", err) + } + + // TODO: Clean up the plugin files if the installation fails. + // TODO: Add a rollback mechanism. + log.Println("Plugin installed successfully") +} diff --git a/config/config.go b/config/config.go index 1d3f132b..f279d295 100644 --- a/config/config.go +++ b/config/config.go @@ -167,7 +167,7 @@ func (c *Config) LoadDefaults(ctx context.Context) { if err != nil { span.RecordError(err) span.End() - log.Fatal(fmt.Errorf("failed to unmarshal global configuration: %w", err)) + log.Panic(fmt.Errorf("failed to unmarshal global configuration: %w", err)) } for configObject, configMap := range gconf { @@ -196,7 +196,7 @@ func (c *Config) LoadDefaults(ctx context.Context) { err := fmt.Errorf("unknown config object: %s", configObject) span.RecordError(err) span.End() - log.Fatal(err) + log.Panic(err) } } } @@ -204,7 +204,7 @@ func (c *Config) LoadDefaults(ctx context.Context) { } else if !os.IsNotExist(err) { span.RecordError(err) span.End() - log.Fatal(fmt.Errorf("failed to read global configuration file: %w", err)) + log.Panic(fmt.Errorf("failed to read global configuration file: %w", err)) } c.pluginDefaults = PluginConfig{ @@ -223,7 +223,7 @@ func (c *Config) LoadDefaults(ctx context.Context) { if err := c.GlobalKoanf.Load(structs.Provider(c.globalDefaults, "json"), nil); err != nil { span.RecordError(err) span.End() - log.Fatal(fmt.Errorf("failed to load default global configuration: %w", err)) + log.Panic(fmt.Errorf("failed to load default global configuration: %w", err)) } } @@ -231,7 +231,7 @@ func (c *Config) LoadDefaults(ctx context.Context) { if err := c.PluginKoanf.Load(structs.Provider(c.pluginDefaults, "json"), nil); err != nil { span.RecordError(err) span.End() - log.Fatal(fmt.Errorf("failed to load default plugin configuration: %w", err)) + log.Panic(fmt.Errorf("failed to load default plugin configuration: %w", err)) } } @@ -246,7 +246,7 @@ func (c *Config) LoadGlobalEnvVars(ctx context.Context) { if err := c.GlobalKoanf.Load(loadEnvVars(), nil); err != nil { span.RecordError(err) span.End() - log.Fatal(fmt.Errorf("failed to load environment variables: %w", err)) + log.Panic(fmt.Errorf("failed to load environment variables: %w", err)) } span.End() @@ -260,7 +260,7 @@ func (c *Config) LoadPluginEnvVars(ctx context.Context) { if err := c.PluginKoanf.Load(loadEnvVars(), nil); err != nil { span.RecordError(err) span.End() - log.Fatal(fmt.Errorf("failed to load environment variables: %w", err)) + log.Panic(fmt.Errorf("failed to load environment variables: %w", err)) } span.End() @@ -279,7 +279,7 @@ func (c *Config) LoadGlobalConfigFile(ctx context.Context) { if err := c.GlobalKoanf.Load(file.Provider(c.globalConfigFile), yaml.Parser()); err != nil { span.RecordError(err) span.End() - log.Fatal(fmt.Errorf("failed to load global configuration: %w", err)) + log.Panic(fmt.Errorf("failed to load global configuration: %w", err)) } span.End() @@ -292,7 +292,7 @@ func (c *Config) LoadPluginConfigFile(ctx context.Context) { if err := c.PluginKoanf.Load(file.Provider(c.pluginConfigFile), yaml.Parser()); err != nil { span.RecordError(err) span.End() - log.Fatal(fmt.Errorf("failed to load plugin configuration: %w", err)) + log.Panic(fmt.Errorf("failed to load plugin configuration: %w", err)) } span.End() @@ -307,7 +307,7 @@ func (c *Config) UnmarshalGlobalConfig(ctx context.Context) { }); err != nil { span.RecordError(err) span.End() - log.Fatal(fmt.Errorf("failed to unmarshal global configuration: %w", err)) + log.Panic(fmt.Errorf("failed to unmarshal global configuration: %w", err)) } span.End() @@ -322,7 +322,7 @@ func (c *Config) UnmarshalPluginConfig(ctx context.Context) { }); err != nil { span.RecordError(err) span.End() - log.Fatal(fmt.Errorf("failed to unmarshal plugin configuration: %w", err)) + log.Panic(fmt.Errorf("failed to unmarshal plugin configuration: %w", err)) } span.End() @@ -336,7 +336,7 @@ func (c *Config) MergeGlobalConfig( if err := c.GlobalKoanf.Load(confmap.Provider(updatedGlobalConfig, "."), nil); err != nil { span.RecordError(err) span.End() - log.Fatal(fmt.Errorf("failed to merge global configuration: %w", err)) + log.Panic(fmt.Errorf("failed to merge global configuration: %w", err)) } if err := c.GlobalKoanf.UnmarshalWithConf("", &c.Global, koanf.UnmarshalConf{ @@ -344,7 +344,7 @@ func (c *Config) MergeGlobalConfig( }); err != nil { span.RecordError(err) span.End() - log.Fatal(fmt.Errorf("failed to unmarshal global configuration: %w", err)) + log.Panic(fmt.Errorf("failed to unmarshal global configuration: %w", err)) } span.End() @@ -357,7 +357,7 @@ func (c *Config) ValidateGlobalConfig(ctx context.Context) { if err := c.GlobalKoanf.Unmarshal("", &globalConfig); err != nil { span.RecordError(err) span.End() - log.Fatal( + log.Panic( gerr.ErrValidationFailed.Wrap( fmt.Errorf("failed to unmarshal global configuration: %w", err)), ) @@ -467,6 +467,6 @@ func (c *Config) ValidateGlobalConfig(ctx context.Context) { } span.RecordError(fmt.Errorf("failed to validate global configuration")) span.End() - log.Fatal("failed to validate global configuration") + log.Panic("failed to validate global configuration") } } diff --git a/logging/logger.go b/logging/logger.go index c52087f7..c7dfa7fa 100644 --- a/logging/logger.go +++ b/logging/logger.go @@ -73,7 +73,7 @@ func NewLogger(ctx context.Context, cfg LoggerConfig) zerolog.Logger { if err != nil { span.RecordError(err) span.End() - log.Fatal(err) + log.Panic(err) } outputs = append(outputs, syslogWriter) case config.RSyslog: @@ -82,7 +82,7 @@ func NewLogger(ctx context.Context, cfg LoggerConfig) zerolog.Logger { rsyslogWriter, err := syslog.Dial( cfg.RSyslogNetwork, cfg.RSyslogAddress, cfg.SyslogPriority, config.DefaultSyslogTag) if err != nil { - log.Fatal(err) + log.Panic(err) } outputs = append(outputs, zerolog.SyslogLevelWriter(rsyslogWriter)) default: diff --git a/logging/logger_windows.go b/logging/logger_windows.go index 54a3de52..8a9ca21e 100644 --- a/logging/logger_windows.go +++ b/logging/logger_windows.go @@ -68,9 +68,9 @@ func NewLogger(ctx context.Context, cfg LoggerConfig) zerolog.Logger { }, ) case config.Syslog: - log.Fatal("Syslog is not supported on Windows") + log.Panic("Syslog is not supported on Windows") case config.RSyslog: - log.Fatal("RSyslog is not supported on Windows") + log.Panic("RSyslog is not supported on Windows") default: outputs = append(outputs, consoleWriter) } diff --git a/tracing/tracing.go b/tracing/tracing.go index a93a6f15..9103f2a9 100644 --- a/tracing/tracing.go +++ b/tracing/tracing.go @@ -28,7 +28,7 @@ func OTLPTracer(insecure bool, collectorURL, serviceName string) func(context.Co ), ) if err != nil { - log.Fatal(err) + log.Panic(err) } resources, err := resource.New( @@ -41,7 +41,7 @@ func OTLPTracer(insecure bool, collectorURL, serviceName string) func(context.Co ) if err != nil { // logger.Error().Err(err).Msg("Could not set resources") - log.Fatal(err) + log.Panic(err) } resources, _ = resource.Merge( From 029899d0248a7ae2ffa3f97051e559fff2d7dd37 Mon Sep 17 00:00:00 2001 From: Mostafa Moradian Date: Tue, 5 Sep 2023 21:25:44 +0200 Subject: [PATCH 2/2] Add support for installing plugins from local archives --- cmd/plugin_install.go | 247 +++++++++++++++++++++++++++++++++++++++++- cmd/utils.go | 229 --------------------------------------- 2 files changed, 244 insertions(+), 232 deletions(-) diff --git a/cmd/plugin_install.go b/cmd/plugin_install.go index 2a0f45fe..dcfc3451 100644 --- a/cmd/plugin_install.go +++ b/cmd/plugin_install.go @@ -1,13 +1,20 @@ package cmd import ( + "context" "log" "os" + "path/filepath" + "regexp" + "runtime" "strings" + "github.com/codingsince1985/checksum" "github.com/gatewayd-io/gatewayd/config" "github.com/getsentry/sentry-go" + "github.com/google/go-github/v53/github" "github.com/spf13/cobra" + yamlv3 "gopkg.in/yaml.v3" ) const ( @@ -56,13 +63,247 @@ var pluginInstallCmd = &cobra.Command{ "Invalid URL. Use the following format: github.com/account/repository@version") } + var releaseID int64 + var downloadURL string + var pluginFilename string + var pluginName string + var err error + var checksumsFilename string + var client *github.Client + var account string + if strings.HasPrefix(args[0], GitHubURLPrefix) { - // Pull the plugin from GitHub. - installFromGitHub(cmd, args, pluginOutputDir, pullOnly) + // Validate the URL. + validGitHubURL := regexp.MustCompile(GitHubURLRegex) + if !validGitHubURL.MatchString(args[0]) { + log.Panic( + "Invalid URL. Use the following format: github.com/account/repository@version") + } + + // Get the plugin version. + pluginVersion := LatestVersion + splittedURL := strings.Split(args[0], "@") + // If the version is not specified, use the latest version. + if len(splittedURL) < NumParts { + log.Println("Version not specified. Using latest version") + } + if len(splittedURL) >= NumParts { + pluginVersion = splittedURL[1] + } + + // Get the plugin account and repository. + accountRepo := strings.Split(strings.TrimPrefix(splittedURL[0], GitHubURLPrefix), "/") + if len(accountRepo) != NumParts { + log.Panic( + "Invalid URL. Use the following format: github.com/account/repository@version") + } + account := accountRepo[0] + pluginName := accountRepo[1] + if account == "" || pluginName == "" { + log.Panic( + "Invalid URL. Use the following format: github.com/account/repository@version") + } + + // Get the release artifact from GitHub. + client = github.NewClient(nil) + var release *github.RepositoryRelease + + if pluginVersion == LatestVersion || pluginVersion == "" { + // Get the latest release. + release, _, err = client.Repositories.GetLatestRelease( + context.Background(), account, pluginName) + } else if strings.HasPrefix(pluginVersion, "v") { + // Get an specific release. + release, _, err = client.Repositories.GetReleaseByTag( + context.Background(), account, pluginName, pluginVersion) + } + if err != nil { + log.Panic("The plugin could not be found") + } + + if release == nil { + log.Panic("The plugin could not be found") + } + + // Get the archive extension. + archiveExt := ExtOthers + if runtime.GOOS == "windows" { + archiveExt = ExtWindows + } + + // Find and download the plugin binary from the release assets. + pluginFilename, downloadURL, releaseID = findAsset(release, func(name string) bool { + return strings.Contains(name, runtime.GOOS) && + strings.Contains(name, runtime.GOARCH) && + strings.Contains(name, archiveExt) + }) + if downloadURL != "" && releaseID != 0 { + downloadFile(client, account, pluginName, downloadURL, releaseID, pluginFilename) + } else { + log.Panic("The plugin file could not be found in the release assets") + } + + // Find and download the checksums.txt from the release assets. + checksumsFilename, downloadURL, releaseID = findAsset(release, func(name string) bool { + return strings.Contains(name, "checksums.txt") + }) + if checksumsFilename != "" && downloadURL != "" && releaseID != 0 { + downloadFile(client, account, pluginName, downloadURL, releaseID, checksumsFilename) + } else { + log.Panic("The checksum file could not be found in the release assets") + } + + // Read the checksums text file. + checksums, err := os.ReadFile(checksumsFilename) + if err != nil { + log.Panic("There was an error reading the checksums file: ", err) + } + + // Get the checksum for the plugin binary. + sum, err := checksum.SHA256sum(pluginFilename) + if err != nil { + log.Panic("There was an error calculating the checksum: ", err) + } + + // Verify the checksums. + checksumLines := strings.Split(string(checksums), "\n") + for _, line := range checksumLines { + if strings.Contains(line, pluginFilename) { + checksum := strings.Split(line, " ")[0] + if checksum != sum { + log.Panic("Checksum verification failed") + } + + log.Println("Checksum verification passed") + break + } + } + + if pullOnly { + log.Println("Plugin binary downloaded to", pluginFilename) + return + } } else { // Pull the plugin from a local archive. - log.Panic("Local archives are not supported yet") + pluginFilename = filepath.Clean(args[0]) + if _, err := os.Stat(pluginFilename); os.IsNotExist(err) { + log.Panic("The plugin file could not be found") + } } + + // Extract the archive. + var filenames []string + if runtime.GOOS == "windows" { + filenames = extractZip(pluginFilename, pluginOutputDir) + } else { + filenames = extractTarGz(pluginFilename, pluginOutputDir) + } + + // Find the extracted plugin binary. + localPath := "" + pluginFileSum := "" + for _, filename := range filenames { + if strings.Contains(filename, pluginName) { + log.Println("Plugin binary extracted to", filename) + localPath = filename + // Get the checksum for the extracted plugin binary. + // TODO: Should we verify the checksum using the checksum.txt file instead? + pluginFileSum, err = checksum.SHA256sum(filename) + if err != nil { + log.Panic("There was an error calculating the checksum: ", err) + } + break + } + } + + // TODO: Clean up after installing the plugin. + // https://github.com/gatewayd-io/gatewayd/issues/311 + + // Create a new gatewayd_plugins.yaml file if it doesn't exist. + if _, err := os.Stat(pluginConfigFile); os.IsNotExist(err) { + generateConfig(cmd, Plugins, pluginConfigFile, false) + } + + // Read the gatewayd_plugins.yaml file. + pluginsConfig, err := os.ReadFile(pluginConfigFile) + if err != nil { + log.Panic(err) + } + + // Get the registered plugins from the plugins configuration file. + var localPluginsConfig map[string]interface{} + if err := yamlv3.Unmarshal(pluginsConfig, &localPluginsConfig); err != nil { + log.Panic("Failed to unmarshal the plugins configuration file: ", err) + } + pluginsList, ok := localPluginsConfig["plugins"].([]interface{}) //nolint:varnamelen + if !ok { + log.Panic("There was an error reading the plugins file from disk") + } + + var contents string + if strings.HasPrefix(args[0], GitHubURLPrefix) { + // Get the list of files in the repository. + var repoContents *github.RepositoryContent + repoContents, _, _, err = client.Repositories.GetContents( + context.Background(), account, pluginName, DefaultPluginConfigFilename, nil) + if err != nil { + log.Panic("There was an error getting the default plugins configuration file: ", err) + } + // Get the contents of the file. + contents, err = repoContents.GetContent() + if err != nil { + log.Panic("There was an error getting the default plugins configuration file: ", err) + } + } else { + // Get the contents of the file. + contentsBytes, err := os.ReadFile( + filepath.Join(pluginOutputDir, DefaultPluginConfigFilename)) + if err != nil { + log.Panic("There was an error getting the default plugins configuration file: ", err) + } + contents = string(contentsBytes) + } + + // Get the plugin configuration from the downloaded plugins configuration file. + var downloadedPluginConfig map[string]interface{} + if err := yamlv3.Unmarshal([]byte(contents), &downloadedPluginConfig); err != nil { + log.Panic("Failed to unmarshal the downloaded plugins configuration file: ", err) + } + defaultPluginConfig, ok := downloadedPluginConfig["plugins"].([]interface{}) + if !ok { + log.Panic("There was an error reading the plugins file from the repository") + } + // Get the plugin configuration. + pluginConfig, ok := defaultPluginConfig[0].(map[string]interface{}) + if !ok { + log.Panic("There was an error reading the default plugin configuration") + } + + // Update the plugin's local path and checksum. + pluginConfig["localPath"] = localPath + pluginConfig["checksum"] = pluginFileSum + + // TODO: Check if the plugin is already installed. + // https://github.com/gatewayd-io/gatewayd/issues/312 + + // Add the plugin config to the list of plugin configs. + pluginsList = append(pluginsList, pluginConfig) + // Merge the result back into the config map. + localPluginsConfig["plugins"] = pluginsList + + // Marshal the map into YAML. + updatedPlugins, err := yamlv3.Marshal(localPluginsConfig) + if err != nil { + log.Panic("There was an error marshalling the plugins configuration: ", err) + } + + // Write the YAML to the plugins config file. + if err = os.WriteFile(pluginConfigFile, updatedPlugins, FilePermissions); err != nil { + log.Panic("There was an error writing the plugins configuration file: ", err) + } + + // TODO: Add a rollback mechanism. + log.Println("Plugin installed successfully") }, } diff --git a/cmd/utils.go b/cmd/utils.go index 83f2d8d4..bc8115d1 100644 --- a/cmd/utils.go +++ b/cmd/utils.go @@ -13,11 +13,8 @@ import ( "os" "path" "path/filepath" - "regexp" - "runtime" "strings" - "github.com/codingsince1985/checksum" "github.com/gatewayd-io/gatewayd/config" "github.com/google/go-github/v53/github" jsonSchemaGenerator "github.com/invopop/jsonschema" @@ -26,7 +23,6 @@ import ( "github.com/knadh/koanf/parsers/yaml" jsonSchemaV5 "github.com/santhosh-tekuri/jsonschema/v5" "github.com/spf13/cobra" - yamlv3 "gopkg.in/yaml.v3" ) type ( @@ -452,228 +448,3 @@ func downloadFile( log.Println("Download completed successfully") } - -func installFromGitHub(cmd *cobra.Command, args []string, pluginOutputDir string, pullOnly bool) { - // Validate the URL. - validGitHubURL := regexp.MustCompile(GitHubURLRegex) - if !validGitHubURL.MatchString(args[0]) { - log.Panic( - "Invalid URL. Use the following format: github.com/account/repository@version") - } - - // Get the plugin version. - pluginVersion := LatestVersion - splittedURL := strings.Split(args[0], "@") - // If the version is not specified, use the latest version. - if len(splittedURL) < NumParts { - log.Println("Version not specified. Using latest version") - } - if len(splittedURL) >= NumParts { - pluginVersion = splittedURL[1] - } - - // Get the plugin account and repository. - accountRepo := strings.Split(strings.TrimPrefix(splittedURL[0], GitHubURLPrefix), "/") - if len(accountRepo) != NumParts { - log.Panic( - "Invalid URL. Use the following format: github.com/account/repository@version") - } - account := accountRepo[0] - pluginName := accountRepo[1] - if account == "" || pluginName == "" { - log.Panic( - "Invalid URL. Use the following format: github.com/account/repository@version") - } - - // Get the release artifact from GitHub. - client := github.NewClient(nil) - var release *github.RepositoryRelease - var err error - if pluginVersion == LatestVersion || pluginVersion == "" { - // Get the latest release. - release, _, err = client.Repositories.GetLatestRelease( - context.Background(), account, pluginName) - } else if strings.HasPrefix(pluginVersion, "v") { - // Get an specific release. - release, _, err = client.Repositories.GetReleaseByTag( - context.Background(), account, pluginName, pluginVersion) - } - if err != nil { - log.Panic("The plugin could not be found") - } - - if release == nil { - log.Panic("The plugin could not be found") - } - - // Get the archive extension. - archiveExt := ExtOthers - if runtime.GOOS == "windows" { - archiveExt = ExtWindows - } - - // Find and download the plugin binary from the release assets. - pluginFilename, downloadURL, releaseID := findAsset(release, func(name string) bool { - return strings.Contains(name, runtime.GOOS) && - strings.Contains(name, runtime.GOARCH) && - strings.Contains(name, archiveExt) - }) - if downloadURL != "" && releaseID != 0 { - downloadFile(client, account, pluginName, downloadURL, releaseID, pluginFilename) - } else { - log.Panic("The plugin file could not be found in the release assets") - } - - // Find and download the checksums.txt from the release assets. - checksumsFilename, downloadURL, releaseID := findAsset(release, func(name string) bool { - return strings.Contains(name, "checksums.txt") - }) - if checksumsFilename != "" && downloadURL != "" && releaseID != 0 { - downloadFile(client, account, pluginName, downloadURL, releaseID, checksumsFilename) - } else { - log.Panic("The checksum file could not be found in the release assets") - } - - // Read the checksums text file. - checksums, err := os.ReadFile(checksumsFilename) - if err != nil { - log.Panic("There was an error reading the checksums file: ", err) - } - - // Get the checksum for the plugin binary. - sum, err := checksum.SHA256sum(pluginFilename) - if err != nil { - log.Panic("There was an error calculating the checksum: ", err) - } - - // Verify the checksums. - checksumLines := strings.Split(string(checksums), "\n") - for _, line := range checksumLines { - if strings.Contains(line, pluginFilename) { - checksum := strings.Split(line, " ")[0] - if checksum != sum { - log.Panic("Checksum verification failed") - } - - log.Println("Checksum verification passed") - break - } - } - - if pullOnly { - log.Println("Plugin binary downloaded to", pluginFilename) - return - } - - // Extract the archive. - var filenames []string - if runtime.GOOS == "windows" { - filenames = extractZip(pluginFilename, pluginOutputDir) - } else { - filenames = extractTarGz(pluginFilename, pluginOutputDir) - } - - // Find the extracted plugin binary. - localPath := "" - pluginFileSum := "" - for _, filename := range filenames { - if strings.Contains(filename, pluginName) { - log.Println("Plugin binary extracted to", filename) - localPath = filename - // Get the checksum for the extracted plugin binary. - // TODO: Should we verify the checksum using the checksum.txt file instead? - pluginFileSum, err = checksum.SHA256sum(filename) - if err != nil { - log.Panic("There was an error calculating the checksum: ", err) - } - break - } - } - - // Remove the tar.gz file. - err = os.Remove(pluginFilename) - if err != nil { - log.Panic("There was an error removing the downloaded plugin file: ", err) - } - - // Remove the checksums.txt file. - err = os.Remove(checksumsFilename) - if err != nil { - log.Panic("There was an error removing the checksums file: ", err) - } - - // Create a new gatewayd_plugins.yaml file if it doesn't exist. - if _, err := os.Stat(pluginConfigFile); os.IsNotExist(err) { - generateConfig(cmd, Plugins, pluginConfigFile, false) - } - - // Read the gatewayd_plugins.yaml file. - pluginsConfig, err := os.ReadFile(pluginConfigFile) - if err != nil { - log.Panic(err) - } - - // Get the registered plugins from the plugins configuration file. - var localPluginsConfig map[string]interface{} - if err := yamlv3.Unmarshal(pluginsConfig, &localPluginsConfig); err != nil { - log.Panic("Failed to unmarshal the plugins configuration file: ", err) - } - pluginsList, ok := localPluginsConfig["plugins"].([]interface{}) //nolint:varnamelen - if !ok { - log.Panic("There was an error reading the plugins file from disk") - } - - // Get the list of files in the repository. - var repoContents *github.RepositoryContent - repoContents, _, _, err = client.Repositories.GetContents( - context.Background(), account, pluginName, DefaultPluginConfigFilename, nil) - if err != nil { - log.Panic("There was an error getting the default plugins configuration file: ", err) - } - // Get the contents of the file. - contents, err := repoContents.GetContent() - if err != nil { - log.Panic("There was an error getting the default plugins configuration file: ", err) - } - - // Get the plugin configuration from the downloaded plugins configuration file. - var downloadedPluginConfig map[string]interface{} - if err := yamlv3.Unmarshal([]byte(contents), &downloadedPluginConfig); err != nil { - log.Panic("Failed to unmarshal the downloaded plugins configuration file: ", err) - } - defaultPluginConfig, ok := downloadedPluginConfig["plugins"].([]interface{}) - if !ok { - log.Panic("There was an error reading the plugins file from the repository") - } - // Get the plugin configuration. - pluginConfig, ok := defaultPluginConfig[0].(map[string]interface{}) - if !ok { - log.Panic("There was an error reading the default plugin configuration") - } - - // Update the plugin's local path and checksum. - pluginConfig["localPath"] = localPath - pluginConfig["checksum"] = pluginFileSum - - // TODO: Check if the plugin is already installed. - - // Add the plugin config to the list of plugin configs. - pluginsList = append(pluginsList, pluginConfig) - // Merge the result back into the config map. - localPluginsConfig["plugins"] = pluginsList - - // Marshal the map into YAML. - updatedPlugins, err := yamlv3.Marshal(localPluginsConfig) - if err != nil { - log.Panic("There was an error marshalling the plugins configuration: ", err) - } - - // Write the YAML to the plugins config file. - if err = os.WriteFile(pluginConfigFile, updatedPlugins, FilePermissions); err != nil { - log.Panic("There was an error writing the plugins configuration file: ", err) - } - - // TODO: Clean up the plugin files if the installation fails. - // TODO: Add a rollback mechanism. - log.Println("Plugin installed successfully") -}