| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756 |
- package registry
- import (
- "context"
- "encoding/base64"
- "encoding/json"
- "errors"
- "fmt"
- "net/http"
- "net/url"
- "strings"
- "sync"
- "time"
- artifactregistry "cloud.google.com/go/artifactregistry/apiv1beta2"
- "github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
- "github.com/Azure/azure-sdk-for-go/sdk/azidentity"
- "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerregistry/armcontainerregistry"
- "github.com/aws/aws-sdk-go-v2/service/ecr"
- ecrTypes "github.com/aws/aws-sdk-go-v2/service/ecr/types"
- "github.com/digitalocean/godo"
- "github.com/docker/cli/cli/config/configfile"
- "github.com/docker/cli/cli/config/types"
- "github.com/docker/distribution/reference"
- ptypes "github.com/porter-dev/porter/api/types"
- "github.com/porter-dev/porter/internal/models"
- ints "github.com/porter-dev/porter/internal/models/integrations"
- "github.com/porter-dev/porter/internal/oauth"
- "github.com/porter-dev/porter/internal/repository"
- "golang.org/x/oauth2"
- v1artifactregistry "google.golang.org/api/artifactregistry/v1"
- "google.golang.org/api/iterator"
- "google.golang.org/api/option"
- artifactregistrypb "google.golang.org/genproto/googleapis/devtools/artifactregistry/v1beta2"
- )
- // Registry wraps the gorm Registry model
- type Registry models.Registry
- func GetECRRegistryURL(awsIntRepo repository.AWSIntegrationRepository, projectID, awsIntID uint) (string, error) {
- ctx := context.Background()
- awsInt, err := awsIntRepo.ReadAWSIntegration(projectID, awsIntID)
- if err != nil {
- return "", err
- }
- svc := ecr.NewFromConfig(awsInt.Config())
- output, err := svc.GetAuthorizationToken(ctx, &ecr.GetAuthorizationTokenInput{})
- if err != nil {
- return "", err
- }
- return *output.AuthorizationData[0].ProxyEndpoint, nil
- }
- // ListRepositories lists the repositories for a registry
- func (r *Registry) ListRepositories(
- repo repository.Repository,
- doAuth *oauth2.Config, // only required if using DOCR
- ) ([]*ptypes.RegistryRepository, error) {
- // switch on the auth mechanism to get a token
- if r.AWSIntegrationID != 0 {
- return r.listECRRepositories(repo)
- }
- if r.GCPIntegrationID != 0 {
- if strings.Contains(r.URL, "pkg.dev") {
- return r.listGARRepositories(repo)
- }
- return r.listGCRRepositories(repo)
- }
- if r.DOIntegrationID != 0 {
- return r.listDOCRRepositories(repo, doAuth)
- }
- if r.AzureIntegrationID != 0 {
- return r.listACRRepositories(repo)
- }
- if r.BasicIntegrationID != 0 {
- return r.listPrivateRegistryRepositories(repo)
- }
- return nil, fmt.Errorf("error listing repositories")
- }
- type gcrJWT struct {
- AccessToken string `json:"token"`
- ExpiresInSec int `json:"expires_in"`
- }
- type gcrErr struct {
- Code string `json:"code"`
- Message string `json:"message"`
- }
- type gcrRepositoryResp struct {
- Repositories []string `json:"repositories"`
- Errors []gcrErr `json:"errors"`
- }
- func (r *Registry) GetGCRToken(repo repository.Repository) (*oauth2.Token, error) {
- getTokenCache := r.getTokenCacheFunc(repo)
- gcp, err := repo.GCPIntegration().ReadGCPIntegration(
- r.ProjectID,
- r.GCPIntegrationID,
- )
- if err != nil {
- return nil, err
- }
- // get oauth2 access token
- return gcp.GetBearerToken(
- getTokenCache,
- r.setTokenCacheFunc(repo),
- "https://www.googleapis.com/auth/devstorage.read_write",
- )
- }
- func (r *Registry) listGCRRepositories(
- repo repository.Repository,
- ) ([]*ptypes.RegistryRepository, error) {
- gcp, err := repo.GCPIntegration().ReadGCPIntegration(
- r.ProjectID,
- r.GCPIntegrationID,
- )
- if err != nil {
- return nil, err
- }
- // Just use service account key to authenticate, since scopes may not be in place
- // for oauth. This also prevents us from making more requests.
- client := &http.Client{}
- regURL := r.URL
- if !strings.HasPrefix(regURL, "http") {
- regURL = fmt.Sprintf("https://%s", regURL)
- }
- regURLParsed, err := url.Parse(regURL)
- regHostname := "gcr.io"
- if err == nil {
- regHostname = regURLParsed.Host
- }
- req, err := http.NewRequest(
- "GET",
- fmt.Sprintf("https://%s/v2/_catalog", regHostname),
- nil,
- )
- if err != nil {
- return nil, err
- }
- req.SetBasicAuth("_json_key", string(gcp.GCPKeyData))
- resp, err := client.Do(req)
- if err != nil {
- return nil, err
- }
- gcrResp := gcrRepositoryResp{}
- if err := json.NewDecoder(resp.Body).Decode(&gcrResp); err != nil {
- return nil, fmt.Errorf("Could not read GCR repositories: %v", err)
- }
- if len(gcrResp.Errors) > 0 {
- errMsg := ""
- for _, gcrErr := range gcrResp.Errors {
- errMsg += fmt.Sprintf(": Code %s, message %s", gcrErr.Code, gcrErr.Message)
- }
- return nil, fmt.Errorf(errMsg)
- }
- res := make([]*ptypes.RegistryRepository, 0)
- parsedURL, err := url.Parse("https://" + r.URL)
- if err != nil {
- return nil, err
- }
- for _, repo := range gcrResp.Repositories {
- res = append(res, &ptypes.RegistryRepository{
- Name: repo,
- URI: parsedURL.Host + "/" + repo,
- })
- }
- return res, nil
- }
- func (r *Registry) GetGARToken(repo repository.Repository) (*oauth2.Token, error) {
- getTokenCache := r.getTokenCacheFunc(repo)
- gcp, err := repo.GCPIntegration().ReadGCPIntegration(
- r.ProjectID,
- r.GCPIntegrationID,
- )
- if err != nil {
- return nil, err
- }
- // get oauth2 access token
- return gcp.GetBearerToken(
- getTokenCache,
- r.setTokenCacheFunc(repo),
- "https://www.googleapis.com/auth/cloud-platform",
- )
- }
- type garTokenSource struct {
- reg *Registry
- repo repository.Repository
- }
- func (source *garTokenSource) Token() (*oauth2.Token, error) {
- return source.reg.GetGARToken(source.repo)
- }
- // GAR has the concept of a "repository" which is a collection of images, unlike ECR or others
- // where a repository is a single image. This function returns the list of fully qualified names
- // of GAR images including their repository names.
- func (r *Registry) listGARRepositories(
- repo repository.Repository,
- ) ([]*ptypes.RegistryRepository, error) {
- gcpInt, err := repo.GCPIntegration().ReadGCPIntegration(
- r.ProjectID,
- r.GCPIntegrationID,
- )
- if err != nil {
- return nil, err
- }
- client, err := artifactregistry.NewClient(context.Background(), option.WithTokenSource(&garTokenSource{
- reg: r,
- repo: repo,
- }), option.WithScopes("roles/artifactregistry.reader"))
- if err != nil {
- return nil, err
- }
- var repoNames []string
- nextToken := ""
- parsedURL, err := url.Parse("https://" + r.URL)
- if err != nil {
- return nil, err
- }
- location := strings.TrimSuffix(parsedURL.Host, "-docker.pkg.dev")
- for {
- it := client.ListRepositories(context.Background(), &artifactregistrypb.ListRepositoriesRequest{
- Parent: fmt.Sprintf("projects/%s/locations/%s", gcpInt.GCPProjectID, location),
- PageSize: 1000,
- PageToken: nextToken,
- })
- for {
- resp, err := it.Next()
- if err == iterator.Done {
- break
- } else if err != nil {
- return nil, err
- }
- if resp.GetFormat() == artifactregistrypb.Repository_DOCKER { // we only care about
- repoSlice := strings.Split(resp.GetName(), "/")
- repoName := repoSlice[len(repoSlice)-1]
- repoNames = append(repoNames, repoName)
- }
- }
- if it.PageInfo().Token == "" {
- break
- }
- nextToken = it.PageInfo().Token
- }
- svc, err := v1artifactregistry.NewService(context.Background(), option.WithTokenSource(&garTokenSource{
- reg: r,
- repo: repo,
- }), option.WithScopes("roles/artifactregistry.reader"))
- if err != nil {
- return nil, err
- }
- nextToken = ""
- dockerSvc := v1artifactregistry.NewProjectsLocationsRepositoriesDockerImagesService(svc)
- var (
- wg sync.WaitGroup
- resMap sync.Map
- )
- for _, repoName := range repoNames {
- wg.Add(1)
- go func(repoName string) {
- defer wg.Done()
- for {
- resp, err := dockerSvc.List(fmt.Sprintf("projects/%s/locations/%s/repositories/%s",
- gcpInt.GCPProjectID, location, repoName)).PageSize(1000).PageToken(nextToken).Do()
- if err != nil {
- // FIXME: we should report this error using a channel
- return
- }
- for _, image := range resp.DockerImages {
- named, err := reference.ParseNamed(image.Uri)
- if err != nil {
- // let us skip this image becaue it has a malformed URI coming from the GCP API
- continue
- }
- uploadTime, _ := time.Parse(time.RFC3339, image.UploadTime)
- resMap.Store(named.Name(), &ptypes.RegistryRepository{
- Name: repoName,
- URI: named.Name(),
- CreatedAt: uploadTime,
- })
- }
- if resp.NextPageToken == "" {
- break
- }
- nextToken = resp.NextPageToken
- }
- }(repoName)
- }
- wg.Wait()
- var res []*ptypes.RegistryRepository
- resMap.Range(func(_, value any) bool {
- res = append(res, value.(*ptypes.RegistryRepository))
- return true
- })
- return res, nil
- }
- func (r *Registry) listECRRepositories(repo repository.Repository) ([]*ptypes.RegistryRepository, error) {
- ctx := context.Background()
- aws, err := repo.AWSIntegration().ReadAWSIntegration(
- r.ProjectID,
- r.AWSIntegrationID,
- )
- if err != nil {
- return nil, err
- }
- svc := ecr.NewFromConfig(aws.Config())
- resp, err := svc.DescribeRepositories(ctx, &ecr.DescribeRepositoriesInput{})
- if err != nil {
- return nil, err
- }
- res := make([]*ptypes.RegistryRepository, 0)
- for _, repo := range resp.Repositories {
- res = append(res, &ptypes.RegistryRepository{
- Name: *repo.RepositoryName,
- CreatedAt: *repo.CreatedAt,
- URI: *repo.RepositoryUri,
- })
- }
- return res, nil
- }
- func (r *Registry) listACRRepositories(repo repository.Repository) ([]*ptypes.RegistryRepository, error) {
- az, err := repo.AzureIntegration().ReadAzureIntegration(
- r.ProjectID,
- r.AzureIntegrationID,
- )
- if err != nil {
- return nil, err
- }
- client := &http.Client{}
- req, err := http.NewRequest(
- "GET",
- fmt.Sprintf("%s/v2/_catalog", r.URL),
- nil,
- )
- if err != nil {
- return nil, err
- }
- req.SetBasicAuth(az.AzureClientID, string(az.ServicePrincipalSecret))
- resp, err := client.Do(req)
- if err != nil {
- return nil, err
- }
- gcrResp := gcrRepositoryResp{}
- if err := json.NewDecoder(resp.Body).Decode(&gcrResp); err != nil {
- return nil, fmt.Errorf("Could not read Azure registry repositories: %v", err)
- }
- res := make([]*ptypes.RegistryRepository, 0)
- if err != nil {
- return nil, err
- }
- for _, repo := range gcrResp.Repositories {
- res = append(res, &ptypes.RegistryRepository{
- Name: repo,
- URI: strings.TrimPrefix(r.URL, "https://") + "/" + repo,
- })
- }
- return res, nil
- }
- // Returns the username/password pair for the registry
- func (r *Registry) GetACRCredentials(repo repository.Repository) (string, string, error) {
- az, err := repo.AzureIntegration().ReadAzureIntegration(
- r.ProjectID,
- r.AzureIntegrationID,
- )
- if err != nil {
- return "", "", err
- }
- // if the passwords and name aren't set, generate them
- if az.ACRTokenName == "" || len(az.ACRPassword1) == 0 {
- az.ACRTokenName = "porter-acr-token"
- // create an acr repo token
- cred, err := azidentity.NewClientSecretCredential(az.AzureTenantID, az.AzureClientID, string(az.ServicePrincipalSecret), nil)
- if err != nil {
- return "", "", err
- }
- scopeMapsClient, err := armcontainerregistry.NewScopeMapsClient(az.AzureSubscriptionID, cred, nil)
- if err != nil {
- return "", "", err
- }
- smRes, err := scopeMapsClient.Get(
- context.Background(),
- az.ACRResourceGroupName,
- az.ACRName,
- "_repositories_admin",
- nil,
- )
- if err != nil {
- return "", "", err
- }
- tokensClient, err := armcontainerregistry.NewTokensClient(az.AzureSubscriptionID, cred, nil)
- if err != nil {
- return "", "", err
- }
- pollerResp, err := tokensClient.BeginCreate(
- context.Background(),
- az.ACRResourceGroupName,
- az.ACRName,
- "porter-acr-token",
- armcontainerregistry.Token{
- Properties: &armcontainerregistry.TokenProperties{
- ScopeMapID: smRes.ID,
- Status: to.Ptr(armcontainerregistry.TokenStatusEnabled),
- },
- },
- nil,
- )
- if err != nil {
- return "", "", err
- }
- tokResp, err := pollerResp.PollUntilDone(context.Background(), 2*time.Second)
- if err != nil {
- return "", "", err
- }
- registriesClient, err := armcontainerregistry.NewRegistriesClient(az.AzureSubscriptionID, cred, nil)
- if err != nil {
- return "", "", err
- }
- poller, err := registriesClient.BeginGenerateCredentials(
- context.Background(),
- az.ACRResourceGroupName,
- az.ACRName,
- armcontainerregistry.GenerateCredentialsParameters{
- TokenID: tokResp.ID,
- },
- &armcontainerregistry.RegistriesClientBeginGenerateCredentialsOptions{ResumeToken: ""})
- if err != nil {
- return "", "", err
- }
- genCredentialsResp, err := poller.PollUntilDone(context.Background(), 2*time.Second)
- if err != nil {
- return "", "", err
- }
- for i, tokPassword := range genCredentialsResp.Passwords {
- if i == 0 {
- az.ACRPassword1 = []byte(*tokPassword.Value)
- } else if i == 1 {
- az.ACRPassword2 = []byte(*tokPassword.Value)
- }
- }
- // update the az integration
- az, err = repo.AzureIntegration().OverwriteAzureIntegration(
- az,
- )
- if err != nil {
- return "", "", err
- }
- }
- return az.ACRTokenName, string(az.ACRPassword1), nil
- }
- func (r *Registry) listDOCRRepositories(
- repo repository.Repository,
- doAuth *oauth2.Config,
- ) ([]*ptypes.RegistryRepository, error) {
- oauthInt, err := repo.OAuthIntegration().ReadOAuthIntegration(
- r.ProjectID,
- r.DOIntegrationID,
- )
- if err != nil {
- return nil, err
- }
- tok, _, err := oauth.GetAccessToken(oauthInt.SharedOAuthModel, doAuth, oauth.MakeUpdateOAuthIntegrationTokenFunction(oauthInt, repo))
- if err != nil {
- return nil, err
- }
- client := godo.NewFromToken(tok)
- urlArr := strings.Split(r.URL, "/")
- if len(urlArr) != 2 {
- return nil, fmt.Errorf("invalid digital ocean registry url")
- }
- name := urlArr[1]
- repos, _, err := client.Registry.ListRepositories(context.TODO(), name, &godo.ListOptions{})
- if err != nil {
- return nil, err
- }
- res := make([]*ptypes.RegistryRepository, 0)
- for _, repo := range repos {
- res = append(res, &ptypes.RegistryRepository{
- Name: repo.Name,
- URI: r.URL + "/" + repo.Name,
- })
- }
- return res, nil
- }
- func (r *Registry) listPrivateRegistryRepositories(
- repo repository.Repository,
- ) ([]*ptypes.RegistryRepository, error) {
- // handle dockerhub different, as it doesn't implement the docker registry http api
- if strings.Contains(r.URL, "docker.io") {
- // in this case, we just return the single dockerhub repository that's linked
- res := make([]*ptypes.RegistryRepository, 0)
- res = append(res, &ptypes.RegistryRepository{
- Name: strings.Split(r.URL, "docker.io/")[1],
- URI: r.URL,
- })
- return res, nil
- }
- basic, err := repo.BasicIntegration().ReadBasicIntegration(
- r.ProjectID,
- r.BasicIntegrationID,
- )
- if err != nil {
- return nil, err
- }
- // Just use service account key to authenticate, since scopes may not be in place
- // for oauth. This also prevents us from making more requests.
- client := &http.Client{}
- // get the host and scheme to make the request
- parsedURL, err := url.Parse(r.URL)
- req, err := http.NewRequest(
- "GET",
- fmt.Sprintf("%s://%s/v2/_catalog", parsedURL.Scheme, parsedURL.Host),
- nil,
- )
- if err != nil {
- return nil, err
- }
- req.SetBasicAuth(string(basic.Username), string(basic.Password))
- resp, err := client.Do(req)
- if err != nil {
- return nil, err
- }
- // if the status code is 404, fallback to the Docker Hub implementation
- if resp.StatusCode == 404 {
- req, err := http.NewRequest(
- "GET",
- fmt.Sprintf("%s/", r.URL),
- nil,
- )
- if err != nil {
- return nil, err
- }
- req.SetBasicAuth(string(basic.Username), string(basic.Password))
- resp, err = client.Do(req)
- if err != nil {
- return nil, err
- }
- }
- gcrResp := gcrRepositoryResp{}
- if err := json.NewDecoder(resp.Body).Decode(&gcrResp); err != nil {
- return nil, fmt.Errorf("Could not read private registry repositories: %v", err)
- }
- res := make([]*ptypes.RegistryRepository, 0)
- if err != nil {
- return nil, err
- }
- for _, repo := range gcrResp.Repositories {
- res = append(res, &ptypes.RegistryRepository{
- Name: repo,
- URI: parsedURL.Host + "/" + repo,
- })
- }
- return res, nil
- }
- func (r *Registry) getTokenCacheFunc(
- repo repository.Repository,
- ) ints.GetTokenCacheFunc {
- return func() (tok *ints.TokenCache, err error) {
- reg, err := repo.Registry().ReadRegistry(r.ProjectID, r.ID)
- if err != nil {
- return nil, err
- }
- return ®.TokenCache.TokenCache, nil
- }
- }
- func (r *Registry) setTokenCacheFunc(
- repo repository.Repository,
- ) ints.SetTokenCacheFunc {
- return func(token string, expiry time.Time) error {
- _, err := repo.Registry().UpdateRegistryTokenCache(
- &ints.RegTokenCache{
- TokenCache: ints.TokenCache{
- Token: []byte(token),
- Expiry: expiry,
- },
- RegistryID: r.ID,
- },
- )
- return err
- }
- }
- // CreateRepository creates a repository for a registry, if needed
- // (currently only required for ECR)
- func (r *Registry) CreateRepository(
- repo repository.Repository,
- name string,
- ) error {
- // if aws, create repository
- if r.AWSIntegrationID != 0 {
- return r.createECRRepository(repo, name)
- } else if r.GCPIntegrationID != 0 && strings.Contains(r.URL, "pkg.dev") {
- return r.createGARRepository(repo, name)
- }
- // otherwise, no-op
- return nil
- }
- func (r *Registry) createECRRepository(
- repo repository.Repository,
- name string,
- ) error {
- ctx := context.Background()
- aws, err := repo.AWSIntegration().ReadAWSIntegration(
- r.ProjectID,
- r.AWSIntegrationID,
- )
- if err != nil {
- return err
- }
- svc := ecr.NewFromConfig(aws.Config())
- // determine if repository already exists
- _, err = svc.DescribeRepositories(ctx, &ecr.DescribeRepositoriesInput{
- RepositoryNames: []string{name},
- })
- if err != nil {
- // if the repository was not found, create it
- var nsk *ecrTypes.RegistryPolicyNotFoundException
- if errors.As(err, &nsk) {
- _, err = svc.CreateRepository(ctx, &ecr.CreateRepositoryInput{
- RepositoryName: &name,
- })
- if err != nil {
- return err
- }
- }
- return err
- }
- return nil
- }
- func (r *Registry) createGARRepository(
- repo repository.Repository,
- name string,
- ) error {
- gcpInt, err := repo.GCPIntegration().ReadGCPIntegration(
- r.ProjectID,
- r.GCPIntegrationID,
- )
- if err != nil {
- return err
- }
- client, err := artifactregistry.NewClient(context.Background(), option.WithTokenSource(&garTokenSource{
- reg: r,
- repo: repo,
- }), option.WithScopes("roles/artifactregistry.admin"))
- if err != nil {
- return err
- }
- defer client.Close()
- parsedURL, err := url.Parse("https://" + r.URL)
- if err != nil {
- return err
- }
- location := strings.TrimSuffix(parsedURL.Host, "-docker.pkg.dev")
- _, err = client.GetRepository(context.Background(), &artifactregistrypb.GetRepositoryRequest{
- Name: fmt.Sprintf("projects/%s/locations/%s/repositories/%s", gcpInt.GCPProjectID, location, name),
- })
- if err != nil && strings.Contains(err.Error(), "not found") {
- // create a new repository
- _, err := client.CreateRepository(context.Background(), &artifactregistrypb.CreateRepositoryRequest{
- Parent: fmt.Sprintf("projects/%s/locations/%s", gcpInt.GCPProjectID, location),
- RepositoryId: name,
- Repository: &artifactregistrypb.Repository{
- Format: artifactregistrypb.Repository_DOCKER,
- },
- })
- if err != nil {
- return err
- }
- } else if err != nil {
- return err
- }
- return nil
- }
- // ListImages lists the images for an image repository
- func (r *Registry) ListImages(
- repoName string,
- repo repository.Repository,
- doAuth *oauth2.Config, // only required if using DOCR
- ) ([]*ptypes.Image, error) {
- // switch on the auth mechanism to get a token
- if r.AWSIntegrationID != 0 {
- return r.listECRImages(repoName, repo)
- }
- if r.AzureIntegrationID != 0 {
- return r.listACRImages(repoName, repo)
- }
- if r.GCPIntegrationID != 0 {
- if strings.Contains(r.URL, "pkg.dev") {
- return r.listGARImages(repoName, repo)
- }
- return r.listGCRImages(repoName, repo)
- }
- if r.DOIntegrationID != 0 {
- return r.listDOCRImages(repoName, repo, doAuth)
- }
- if r.BasicIntegrationID != 0 {
- return r.listPrivateRegistryImages(repoName, repo)
- }
- return nil, fmt.Errorf("error listing images")
- }
- func (r *Registry) GetECRPaginatedImages(
- repoName string,
- repo repository.Repository,
- maxResults int64,
- nextToken *string,
- ) ([]*ptypes.Image, *string, error) {
- ctx := context.Background()
- aws, err := repo.AWSIntegration().ReadAWSIntegration(
- r.ProjectID,
- r.AWSIntegrationID,
- )
- if err != nil {
- return nil, nil, err
- }
- svc := ecr.NewFromConfig(aws.Config())
- mr := int32(maxResults)
- resp, err := svc.ListImages(ctx, &ecr.ListImagesInput{
- RepositoryName: &repoName,
- MaxResults: &mr,
- NextToken: nextToken,
- })
- if err != nil {
- return nil, nil, err
- }
- if len(resp.ImageIds) == 0 {
- return []*ptypes.Image{}, nil, nil
- }
- imageIDLen := len(resp.ImageIds)
- imageDetails := make([]ecrTypes.ImageDetail, 0)
- imageIDMap := make(map[string]bool)
- for _, id := range resp.ImageIds {
- if id.ImageDigest != nil && id.ImageTag != nil {
- imageIDMap[*id.ImageTag] = true
- }
- }
- var wg sync.WaitGroup
- var mu sync.Mutex
- // AWS API expects the length of imageIDs to be at max 100 at a time
- for start := 0; start < imageIDLen; start += 100 {
- end := start + 100
- if end > imageIDLen {
- end = imageIDLen
- }
- wg.Add(1)
- go func(start, end int) {
- defer wg.Done()
- describeResp, err := svc.DescribeImages(ctx, &ecr.DescribeImagesInput{
- RepositoryName: &repoName,
- ImageIds: resp.ImageIds[start:end],
- })
- if err != nil {
- return
- }
- mu.Lock()
- imageDetails = append(imageDetails, describeResp.ImageDetails...)
- mu.Unlock()
- }(start, end)
- }
- wg.Wait()
- res := make([]*ptypes.Image, 0)
- imageInfoMap := make(map[string]*ptypes.Image)
- for _, img := range imageDetails {
- for _, tag := range img.ImageTags {
- newImage := &ptypes.Image{
- Digest: *img.ImageDigest,
- Tag: tag,
- RepositoryName: repoName,
- PushedAt: img.ImagePushedAt,
- }
- if _, ok := imageIDMap[tag]; ok {
- if _, ok := imageInfoMap[tag]; !ok {
- imageInfoMap[tag] = newImage
- }
- }
- if len(imageInfoMap) == int(maxResults) {
- break
- }
- }
- if len(imageInfoMap) == int(maxResults) {
- break
- }
- }
- for _, v := range imageInfoMap {
- res = append(res, v)
- }
- return res, resp.NextToken, nil
- }
- func (r *Registry) listECRImages(repoName string, repo repository.Repository) ([]*ptypes.Image, error) {
- ctx := context.Background()
- aws, err := repo.AWSIntegration().ReadAWSIntegration(
- r.ProjectID,
- r.AWSIntegrationID,
- )
- if err != nil {
- return nil, err
- }
- svc := ecr.NewFromConfig(aws.Config())
- maxResults := int64(1000)
- var imageIDs []ecrTypes.ImageIdentifier
- mr := int32(maxResults)
- resp, err := svc.ListImages(ctx, &ecr.ListImagesInput{
- RepositoryName: &repoName,
- MaxResults: &mr,
- })
- if err != nil {
- return nil, err
- }
- if len(resp.ImageIds) == 0 {
- return []*ptypes.Image{}, nil
- }
- imageIDs = append(imageIDs, resp.ImageIds...)
- nextToken := resp.NextToken
- for nextToken != nil {
- resp, err := svc.ListImages(ctx, &ecr.ListImagesInput{
- RepositoryName: &repoName,
- MaxResults: &mr,
- NextToken: nextToken,
- })
- if err != nil {
- return nil, err
- }
- imageIDs = append(imageIDs, resp.ImageIds...)
- nextToken = resp.NextToken
- }
- imageIDLen := len(imageIDs)
- imageDetails := make([]ecrTypes.ImageDetail, 0)
- var wg sync.WaitGroup
- var mu sync.Mutex
- // AWS API expects the length of imageIDs to be at max 100 at a time
- for start := 0; start < imageIDLen; start += 100 {
- end := start + 100
- if end > imageIDLen {
- end = imageIDLen
- }
- wg.Add(1)
- go func(start, end int) {
- defer wg.Done()
- describeResp, err := svc.DescribeImages(ctx, &ecr.DescribeImagesInput{
- RepositoryName: &repoName,
- ImageIds: imageIDs[start:end],
- })
- if err != nil {
- return
- }
- mu.Lock()
- imageDetails = append(imageDetails, describeResp.ImageDetails...)
- mu.Unlock()
- }(start, end)
- }
- wg.Wait()
- res := make([]*ptypes.Image, 0)
- imageInfoMap := make(map[string]*ptypes.Image)
- for _, img := range imageDetails {
- for _, tag := range img.ImageTags {
- newImage := &ptypes.Image{
- Digest: *img.ImageDigest,
- Tag: tag,
- RepositoryName: repoName,
- PushedAt: img.ImagePushedAt,
- }
- if _, ok := imageInfoMap[tag]; !ok {
- imageInfoMap[tag] = newImage
- }
- }
- }
- for _, v := range imageInfoMap {
- res = append(res, v)
- }
- return res, nil
- }
- func (r *Registry) listACRImages(repoName string, repo repository.Repository) ([]*ptypes.Image, error) {
- az, err := repo.AzureIntegration().ReadAzureIntegration(
- r.ProjectID,
- r.AzureIntegrationID,
- )
- if err != nil {
- return nil, err
- }
- // use JWT token to request catalog
- client := &http.Client{}
- req, err := http.NewRequest(
- "GET",
- fmt.Sprintf("%s/v2/%s/tags/list", r.URL, repoName),
- nil,
- )
- if err != nil {
- return nil, err
- }
- req.SetBasicAuth(az.AzureClientID, string(az.ServicePrincipalSecret))
- resp, err := client.Do(req)
- if err != nil {
- return nil, err
- }
- gcrResp := gcrImageResp{}
- if err := json.NewDecoder(resp.Body).Decode(&gcrResp); err != nil {
- return nil, fmt.Errorf("Could not read GCR repositories: %v", err)
- }
- res := make([]*ptypes.Image, 0)
- for _, tag := range gcrResp.Tags {
- res = append(res, &ptypes.Image{
- RepositoryName: strings.TrimPrefix(repoName, "https://"),
- Tag: tag,
- })
- }
- return res, nil
- }
- type gcrImageResp struct {
- Tags []string `json:"tags"`
- }
- func (r *Registry) listGCRImages(repoName string, repo repository.Repository) ([]*ptypes.Image, error) {
- gcp, err := repo.GCPIntegration().ReadGCPIntegration(
- r.ProjectID,
- r.GCPIntegrationID,
- )
- if err != nil {
- return nil, err
- }
- // use JWT token to request catalog
- client := &http.Client{}
- parsedURL, err := url.Parse("https://" + r.URL)
- if err != nil {
- return nil, err
- }
- trimmedPath := strings.Trim(parsedURL.Path, "/")
- req, err := http.NewRequest(
- "GET",
- fmt.Sprintf("https://%s/v2/%s/%s/tags/list", parsedURL.Host, trimmedPath, repoName),
- nil,
- )
- if err != nil {
- return nil, err
- }
- req.SetBasicAuth("_json_key", string(gcp.GCPKeyData))
- resp, err := client.Do(req)
- if err != nil {
- return nil, err
- }
- gcrResp := gcrImageResp{}
- if err := json.NewDecoder(resp.Body).Decode(&gcrResp); err != nil {
- return nil, fmt.Errorf("Could not read GCR repositories: %v", err)
- }
- res := make([]*ptypes.Image, 0)
- for _, tag := range gcrResp.Tags {
- res = append(res, &ptypes.Image{
- RepositoryName: repoName,
- Tag: tag,
- })
- }
- return res, nil
- }
- func (r *Registry) listGARImages(repoName string, repo repository.Repository) ([]*ptypes.Image, error) {
- repoImageSlice := strings.Split(repoName, "/")
- if len(repoImageSlice) != 2 {
- return nil, fmt.Errorf("invalid GAR repo name: %s. Expected to be in the form of REPOSITORY/IMAGE", repoName)
- }
- gcpInt, err := repo.GCPIntegration().ReadGCPIntegration(
- r.ProjectID,
- r.GCPIntegrationID,
- )
- if err != nil {
- return nil, err
- }
- svc, err := v1artifactregistry.NewService(context.Background(), option.WithTokenSource(&garTokenSource{
- reg: r,
- repo: repo,
- }), option.WithScopes("roles/artifactregistry.reader"))
- if err != nil {
- return nil, err
- }
- var res []*ptypes.Image
- parsedURL, err := url.Parse("https://" + r.URL)
- if err != nil {
- return nil, err
- }
- location := strings.TrimSuffix(parsedURL.Host, "-docker.pkg.dev")
- dockerSvc := v1artifactregistry.NewProjectsLocationsRepositoriesDockerImagesService(svc)
- nextToken := ""
- for {
- resp, err := dockerSvc.List(fmt.Sprintf("projects/%s/locations/%s/repositories/%s",
- gcpInt.GCPProjectID, location, repoImageSlice[0])).PageSize(1000).PageToken(nextToken).Do()
- if err != nil {
- return nil, err
- }
- for _, image := range resp.DockerImages {
- named, err := reference.ParseNamed(image.Uri)
- if err != nil {
- continue
- }
- paths := strings.Split(reference.Path(named), "/")
- imageName := paths[len(paths)-1]
- if imageName == repoImageSlice[1] {
- uploadTime, _ := time.Parse(time.RFC3339, image.UploadTime)
- for _, tag := range image.Tags {
- res = append(res, &ptypes.Image{
- RepositoryName: repoName,
- Tag: tag,
- PushedAt: &uploadTime,
- Digest: strings.Split(image.Uri, "@")[1],
- })
- }
- }
- }
- if resp.NextPageToken == "" {
- break
- }
- nextToken = resp.NextPageToken
- }
- return res, nil
- }
- func (r *Registry) listDOCRImages(
- repoName string,
- repo repository.Repository,
- doAuth *oauth2.Config,
- ) ([]*ptypes.Image, error) {
- oauthInt, err := repo.OAuthIntegration().ReadOAuthIntegration(
- r.ProjectID,
- r.DOIntegrationID,
- )
- if err != nil {
- return nil, err
- }
- tok, _, err := oauth.GetAccessToken(oauthInt.SharedOAuthModel, doAuth, oauth.MakeUpdateOAuthIntegrationTokenFunction(oauthInt, repo))
- if err != nil {
- return nil, err
- }
- client := godo.NewFromToken(tok)
- urlArr := strings.Split(r.URL, "/")
- if len(urlArr) != 2 {
- return nil, fmt.Errorf("invalid digital ocean registry url")
- }
- name := urlArr[1]
- var tags []*godo.RepositoryTag
- opt := &godo.ListOptions{
- PerPage: 200,
- }
- for {
- nextTags, resp, err := client.Registry.ListRepositoryTags(context.TODO(), name, repoName, opt)
- if err != nil {
- return nil, err
- }
- tags = append(tags, nextTags...)
- if resp.Links == nil || resp.Links.IsLastPage() {
- break
- }
- page, err := resp.Links.CurrentPage()
- if err != nil {
- return nil, err
- }
- opt.Page = page + 1
- }
- res := make([]*ptypes.Image, 0)
- for _, tag := range tags {
- res = append(res, &ptypes.Image{
- RepositoryName: repoName,
- Tag: tag.Tag,
- })
- }
- return res, nil
- }
- func (r *Registry) listPrivateRegistryImages(repoName string, repo repository.Repository) ([]*ptypes.Image, error) {
- // handle dockerhub different, as it doesn't implement the docker registry http api
- if strings.Contains(r.URL, "docker.io") {
- return r.listDockerHubImages(repoName, repo)
- }
- basic, err := repo.BasicIntegration().ReadBasicIntegration(
- r.ProjectID,
- r.BasicIntegrationID,
- )
- if err != nil {
- return nil, err
- }
- // Just use service account key to authenticate, since scopes may not be in place
- // for oauth. This also prevents us from making more requests.
- client := &http.Client{}
- // get the host and scheme to make the request
- parsedURL, err := url.Parse(r.URL)
- req, err := http.NewRequest(
- "GET",
- fmt.Sprintf("%s://%s/v2/%s/tags/list", parsedURL.Scheme, parsedURL.Host, repoName),
- nil,
- )
- if err != nil {
- return nil, err
- }
- req.SetBasicAuth(string(basic.Username), string(basic.Password))
- resp, err := client.Do(req)
- if err != nil {
- return nil, err
- }
- gcrResp := gcrImageResp{}
- if err := json.NewDecoder(resp.Body).Decode(&gcrResp); err != nil {
- return nil, fmt.Errorf("Could not read private registry repositories: %v", err)
- }
- res := make([]*ptypes.Image, 0)
- for _, tag := range gcrResp.Tags {
- res = append(res, &ptypes.Image{
- RepositoryName: repoName,
- Tag: tag,
- })
- }
- return res, nil
- }
- type dockerHubImageResult struct {
- Name string `json:"name"`
- }
- type dockerHubImageResp struct {
- Results []dockerHubImageResult `json:"results"`
- }
- type dockerHubLoginReq struct {
- Username string `json:"username"`
- Password string `json:"password"`
- }
- type dockerHubLoginResp struct {
- Token string `json:"token"`
- }
- func (r *Registry) listDockerHubImages(repoName string, repo repository.Repository) ([]*ptypes.Image, error) {
- basic, err := repo.BasicIntegration().ReadBasicIntegration(
- r.ProjectID,
- r.BasicIntegrationID,
- )
- if err != nil {
- return nil, err
- }
- client := &http.Client{}
- // first, make a request for the access token
- data, err := json.Marshal(&dockerHubLoginReq{
- Username: string(basic.Username),
- Password: string(basic.Password),
- })
- if err != nil {
- return nil, err
- }
- req, err := http.NewRequest(
- "POST",
- "https://hub.docker.com/v2/users/login",
- strings.NewReader(string(data)),
- )
- if err != nil {
- return nil, err
- }
- req.Header.Add("Content-Type", "application/json")
- resp, err := client.Do(req)
- if err != nil {
- return nil, err
- }
- tokenObj := dockerHubLoginResp{}
- if err := json.NewDecoder(resp.Body).Decode(&tokenObj); err != nil {
- return nil, fmt.Errorf("Could not decode Dockerhub token from response: %v", err)
- }
- req, err = http.NewRequest(
- "GET",
- fmt.Sprintf("https://hub.docker.com/v2/repositories/%s/tags", strings.Split(r.URL, "docker.io/")[1]),
- nil,
- )
- if err != nil {
- return nil, err
- }
- req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", tokenObj.Token))
- resp, err = client.Do(req)
- if err != nil {
- return nil, err
- }
- imageResp := dockerHubImageResp{}
- if err := json.NewDecoder(resp.Body).Decode(&imageResp); err != nil {
- return nil, fmt.Errorf("Could not read private registry repositories: %v", err)
- }
- res := make([]*ptypes.Image, 0)
- for _, result := range imageResp.Results {
- res = append(res, &ptypes.Image{
- RepositoryName: repoName,
- Tag: result.Name,
- })
- }
- return res, nil
- }
- // GetDockerConfigJSON returns a dockerconfigjson file contents with "auths"
- // populated.
- func (r *Registry) GetDockerConfigJSON(
- repo repository.Repository,
- doAuth *oauth2.Config, // only required if using DOCR
- ) ([]byte, error) {
- var conf *configfile.ConfigFile
- var err error
- // switch on the auth mechanism to get a token
- if r.AWSIntegrationID != 0 {
- conf, err = r.getECRDockerConfigFile(repo)
- }
- if r.GCPIntegrationID != 0 {
- conf, err = r.getGCRDockerConfigFile(repo)
- }
- if r.DOIntegrationID != 0 {
- conf, err = r.getDOCRDockerConfigFile(repo, doAuth)
- }
- if r.BasicIntegrationID != 0 {
- conf, err = r.getPrivateRegistryDockerConfigFile(repo)
- }
- if r.AzureIntegrationID != 0 {
- conf, err = r.getACRDockerConfigFile(repo)
- }
- if err != nil {
- return nil, err
- }
- return json.Marshal(conf)
- }
- func (r *Registry) getECRDockerConfigFile(
- repo repository.Repository,
- ) (*configfile.ConfigFile, error) {
- ctx := context.Background()
- aws, err := repo.AWSIntegration().ReadAWSIntegration(
- r.ProjectID,
- r.AWSIntegrationID,
- )
- if err != nil {
- return nil, err
- }
- svc := ecr.NewFromConfig(aws.Config())
- output, err := svc.GetAuthorizationToken(ctx, &ecr.GetAuthorizationTokenInput{})
- if err != nil {
- return nil, err
- }
- token := *output.AuthorizationData[0].AuthorizationToken
- decodedToken, err := base64.StdEncoding.DecodeString(token)
- if err != nil {
- return nil, err
- }
- parts := strings.SplitN(string(decodedToken), ":", 2)
- if len(parts) < 2 {
- return nil, err
- }
- key := r.URL
- if !strings.Contains(key, "http") {
- key = "https://" + key
- }
- return &configfile.ConfigFile{
- AuthConfigs: map[string]types.AuthConfig{
- key: {
- Username: parts[0],
- Password: parts[1],
- Auth: token,
- },
- },
- }, nil
- }
- func (r *Registry) getGCRDockerConfigFile(
- repo repository.Repository,
- ) (*configfile.ConfigFile, error) {
- gcp, err := repo.GCPIntegration().ReadGCPIntegration(
- r.ProjectID,
- r.GCPIntegrationID,
- )
- if err != nil {
- return nil, err
- }
- key := r.URL
- if !strings.Contains(key, "http") {
- key = "https://" + key
- }
- parsedURL, _ := url.Parse(key)
- return &configfile.ConfigFile{
- AuthConfigs: map[string]types.AuthConfig{
- parsedURL.Host: {
- Username: "_json_key",
- Password: string(gcp.GCPKeyData),
- Auth: generateAuthToken("_json_key", string(gcp.GCPKeyData)),
- },
- },
- }, nil
- }
- func (r *Registry) getDOCRDockerConfigFile(
- repo repository.Repository,
- doAuth *oauth2.Config,
- ) (*configfile.ConfigFile, error) {
- oauthInt, err := repo.OAuthIntegration().ReadOAuthIntegration(
- r.ProjectID,
- r.DOIntegrationID,
- )
- if err != nil {
- return nil, err
- }
- tok, _, err := oauth.GetAccessToken(oauthInt.SharedOAuthModel, doAuth, oauth.MakeUpdateOAuthIntegrationTokenFunction(oauthInt, repo))
- if err != nil {
- return nil, err
- }
- key := r.URL
- if !strings.Contains(key, "http") {
- key = "https://" + key
- }
- parsedURL, _ := url.Parse(key)
- return &configfile.ConfigFile{
- AuthConfigs: map[string]types.AuthConfig{
- parsedURL.Host: {
- Username: tok,
- Password: tok,
- Auth: generateAuthToken(tok, tok),
- },
- },
- }, nil
- }
- func (r *Registry) getPrivateRegistryDockerConfigFile(
- repo repository.Repository,
- ) (*configfile.ConfigFile, error) {
- basic, err := repo.BasicIntegration().ReadBasicIntegration(
- r.ProjectID,
- r.BasicIntegrationID,
- )
- if err != nil {
- return nil, err
- }
- key := r.URL
- if !strings.Contains(key, "http") {
- key = "https://" + key
- }
- parsedURL, _ := url.Parse(key)
- authConfigKey := parsedURL.Host
- if strings.Contains(r.URL, "index.docker.io") {
- authConfigKey = "https://index.docker.io/v1/"
- }
- return &configfile.ConfigFile{
- AuthConfigs: map[string]types.AuthConfig{
- authConfigKey: {
- Username: string(basic.Username),
- Password: string(basic.Password),
- Auth: generateAuthToken(string(basic.Username), string(basic.Password)),
- },
- },
- }, nil
- }
- func (r *Registry) getACRDockerConfigFile(
- repo repository.Repository,
- ) (*configfile.ConfigFile, error) {
- username, pw, err := r.GetACRCredentials(repo)
- if err != nil {
- return nil, err
- }
- key := r.URL
- if !strings.Contains(key, "http") {
- key = "https://" + key
- }
- parsedURL, _ := url.Parse(key)
- return &configfile.ConfigFile{
- AuthConfigs: map[string]types.AuthConfig{
- parsedURL.Host: {
- Username: string(username),
- Password: string(pw),
- Auth: generateAuthToken(string(username), string(pw)),
- },
- },
- }, nil
- }
- func generateAuthToken(username, password string) string {
- return base64.StdEncoding.EncodeToString([]byte(username + ":" + password))
- }
|