Files
steampipe/pkg/plugin/version_checker.go

212 lines
6.5 KiB
Go

package plugin
import (
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/url"
"strings"
"time"
"github.com/turbot/steampipe/pkg/ociinstaller"
"github.com/turbot/steampipe/pkg/ociinstaller/versionfile"
"github.com/turbot/steampipe/pkg/utils"
)
// VersionCheckReport ::
type VersionCheckReport struct {
Plugin *versionfile.InstalledVersion
CheckResponse versionCheckResponsePayload
CheckRequest versionCheckRequestPayload
}
func (vr *VersionCheckReport) ShortName() string {
return fmt.Sprintf("%s/%s", vr.CheckResponse.Org, vr.CheckResponse.Name)
}
func (vr *VersionCheckReport) ShortNameWithStream() string {
// dont show stream names for latest streams
if vr.CheckResponse.Stream != "latest" {
return fmt.Sprintf("%s/%s@%s", vr.CheckResponse.Org, vr.CheckResponse.Name, vr.CheckResponse.Stream)
}
return fmt.Sprintf("%s/%s", vr.CheckResponse.Org, vr.CheckResponse.Name)
}
// VersionChecker :: wrapper struct over the plugin version check utilities
type VersionChecker struct {
pluginsToCheck []*versionfile.InstalledVersion
signature string
}
// GetUpdateReport looks up and reports the updated version of selective turbot plugins which are listed in versions.json
func GetUpdateReport(ctx context.Context, installationID string, check []*versionfile.InstalledVersion) map[string]VersionCheckReport {
versionChecker := new(VersionChecker)
versionChecker.signature = installationID
for _, c := range check {
if strings.HasPrefix(c.Name, ociinstaller.DefaultImageRepoDisplayURL) {
versionChecker.pluginsToCheck = append(versionChecker.pluginsToCheck, c)
}
}
return versionChecker.reportPluginUpdates(ctx)
}
// GetAllUpdateReport looks up and reports the updated version of all turbot plugins which are listed in versions.json
func GetAllUpdateReport(ctx context.Context, installationID string) map[string]VersionCheckReport {
versionChecker := new(VersionChecker)
versionChecker.signature = installationID
versionChecker.pluginsToCheck = []*versionfile.InstalledVersion{}
versionFileData, err := versionfile.LoadPluginVersionFile()
if err != nil {
log.Println("[TRACE]", "CheckAndReportPluginUpdates", "could not load versionfile")
return nil
}
for _, p := range versionFileData.Plugins {
if strings.HasPrefix(p.Name, ociinstaller.DefaultImageRepoDisplayURL) {
versionChecker.pluginsToCheck = append(versionChecker.pluginsToCheck, p)
}
}
return versionChecker.reportPluginUpdates(ctx)
}
func (v *VersionChecker) reportPluginUpdates(ctx context.Context) map[string]VersionCheckReport {
versionFileData, err := versionfile.LoadPluginVersionFile()
if err != nil {
log.Println("[TRACE]", "CheckAndReportPluginUpdates", "could not load versionfile")
return nil
}
if len(v.pluginsToCheck) == 0 {
// there's no plugin installed. no point continuing
return nil
}
reports := v.getLatestVersionsForPlugins(ctx, v.pluginsToCheck)
// remove elements from `reports` which have empty strings in CheckResponse
// this happens if we have sent a plugin to the API which doesn't exist
// in the registry
for key, value := range reports {
if value.CheckResponse.Name == "" {
// delete this key
delete(reports, key)
}
}
// update the version file
for _, plugin := range v.pluginsToCheck {
versionFileData.Plugins[plugin.Name].LastCheckedDate = versionfile.FormatTime(time.Now())
}
if err = versionFileData.Save(); err != nil {
log.Println("[TRACE]", "CheckAndReportPluginUpdates", "could not save versionfile")
return nil
}
return reports
}
func (v *VersionChecker) getLatestVersionsForPlugins(ctx context.Context, plugins []*versionfile.InstalledVersion) map[string]VersionCheckReport {
getMapKey := func(thisPayload versionCheckRequestPayload) string {
return fmt.Sprintf("%s/%s/%s", thisPayload.Org, thisPayload.Name, thisPayload.Stream)
}
var requestPayload []versionCheckRequestPayload
reports := map[string]VersionCheckReport{}
for _, ref := range plugins {
thisPayload := v.getPayloadFromInstalledData(ref)
requestPayload = append(requestPayload, thisPayload)
reports[getMapKey(thisPayload)] = VersionCheckReport{
Plugin: ref,
CheckRequest: thisPayload,
CheckResponse: versionCheckResponsePayload{},
}
}
serverResponse, err := v.requestServerForLatest(ctx, requestPayload)
if err != nil {
log.Printf("[TRACE] PluginVersionChecker getLatestVersionsForPlugins returned error: %s", err.Error())
// return a blank map
return map[string]VersionCheckReport{}
}
for _, pluginResponseData := range serverResponse {
r := reports[pluginResponseData.getMapKey()]
r.CheckResponse = pluginResponseData
reports[pluginResponseData.getMapKey()] = r
}
return reports
}
func (v *VersionChecker) getPayloadFromInstalledData(plugin *versionfile.InstalledVersion) versionCheckRequestPayload {
ref := ociinstaller.NewSteampipeImageRef(plugin.Name)
org, name, stream := ref.GetOrgNameAndStream()
payload := versionCheckRequestPayload{
Org: org,
Name: name,
Stream: stream,
Version: plugin.Version,
Digest: plugin.ImageDigest,
}
// if Digest field is missing, populate with dummy field
// - this will force and update an in doing so fix the versions.json
// https://github.com/turbot/steampipe/issues/2030
if payload.Digest == "" {
payload.Digest = "no digest"
}
return payload
}
func (v *VersionChecker) getVersionCheckURL() url.URL {
var u url.URL
u.Scheme = "https"
u.Host = "hub.steampipe.io"
u.Path = "api/plugin/version"
return u
}
func (v *VersionChecker) requestServerForLatest(ctx context.Context, payload []versionCheckRequestPayload) ([]versionCheckResponsePayload, error) {
// Set a default timeout of 3 sec for the check request (in milliseconds)
sendRequestTo := v.getVersionCheckURL()
requestBody := utils.BuildRequestPayload(v.signature, map[string]interface{}{
"plugins": payload,
})
resp, err := utils.SendRequest(ctx, v.signature, "POST", sendRequestTo, requestBody)
if err != nil {
log.Printf("[TRACE] Could not send request")
return nil, err
}
if resp.StatusCode != 200 {
log.Printf("[TRACE] Unknown response during version check: %d\n", resp.StatusCode)
return nil, fmt.Errorf("requestServerForLatest failed - SendRequest returned %d", resp.StatusCode)
}
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("[TRACE] Error reading body stream: %v", err)
return nil, err
}
defer resp.Body.Close()
var responseData []versionCheckResponsePayload
err = json.Unmarshal(bodyBytes, &responseData)
if err != nil {
log.Println("[TRACE] Error in unmarshalling plugin update response", err)
return nil, err
}
return responseData, nil
}