registry.go 40 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788
  1. package registry
  2. import (
  3. "context"
  4. "encoding/base64"
  5. "encoding/json"
  6. "fmt"
  7. "net/http"
  8. "net/url"
  9. "sort"
  10. "strings"
  11. "sync"
  12. "time"
  13. artifactregistry "cloud.google.com/go/artifactregistry/apiv1beta2"
  14. "github.com/Azure/azure-sdk-for-go/sdk/azidentity"
  15. "github.com/aws/aws-sdk-go/aws/awserr"
  16. "github.com/aws/aws-sdk-go/service/ecr"
  17. "github.com/bufbuild/connect-go"
  18. porterv1 "github.com/porter-dev/api-contracts/generated/go/porter/v1"
  19. "github.com/porter-dev/porter/api/server/shared/config"
  20. "github.com/porter-dev/porter/internal/models"
  21. "github.com/porter-dev/porter/internal/oauth"
  22. "github.com/porter-dev/porter/internal/repository"
  23. "golang.org/x/oauth2"
  24. v1artifactregistry "google.golang.org/api/artifactregistry/v1"
  25. "google.golang.org/api/iterator"
  26. "google.golang.org/api/option"
  27. artifactregistrypb "google.golang.org/genproto/googleapis/devtools/artifactregistry/v1beta2"
  28. ints "github.com/porter-dev/porter/internal/models/integrations"
  29. ptypes "github.com/porter-dev/porter/api/types"
  30. "github.com/digitalocean/godo"
  31. "github.com/docker/cli/cli/config/configfile"
  32. "github.com/docker/cli/cli/config/types"
  33. "github.com/docker/distribution/reference"
  34. "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerregistry/armcontainerregistry"
  35. "github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
  36. )
  37. // Registry wraps the gorm Registry model
  38. type Registry models.Registry
  39. func GetECRRegistryURL(awsIntRepo repository.AWSIntegrationRepository, projectID, awsIntID uint) (string, error) {
  40. awsInt, err := awsIntRepo.ReadAWSIntegration(projectID, awsIntID)
  41. if err != nil {
  42. return "", err
  43. }
  44. sess, err := awsInt.GetSession()
  45. if err != nil {
  46. return "", err
  47. }
  48. ecrSvc := ecr.New(sess)
  49. output, err := ecrSvc.GetAuthorizationToken(&ecr.GetAuthorizationTokenInput{})
  50. if err != nil {
  51. return "", err
  52. }
  53. return *output.AuthorizationData[0].ProxyEndpoint, nil
  54. }
  55. // ListRepositories lists the repositories for a registry
  56. func (r *Registry) ListRepositories(
  57. ctx context.Context,
  58. repo repository.Repository,
  59. conf *config.Config,
  60. ) ([]*ptypes.RegistryRepository, error) {
  61. // switch on the auth mechanism to get a token
  62. if r.AWSIntegrationID != 0 {
  63. aws, err := repo.AWSIntegration().ReadAWSIntegration(
  64. r.ProjectID,
  65. r.AWSIntegrationID,
  66. )
  67. if err != nil {
  68. return nil, err
  69. }
  70. return r.listECRRepositories(aws)
  71. }
  72. if r.GCPIntegrationID != 0 {
  73. if strings.Contains(r.URL, "pkg.dev") {
  74. return r.listGARRepositories(repo)
  75. }
  76. return r.listGCRRepositories(repo)
  77. }
  78. if r.DOIntegrationID != 0 {
  79. return r.listDOCRRepositories(repo, conf.DOConf)
  80. }
  81. if r.AzureIntegrationID != 0 {
  82. return r.listACRRepositories(repo)
  83. }
  84. if r.BasicIntegrationID != 0 {
  85. return r.listPrivateRegistryRepositories(repo)
  86. }
  87. project, err := conf.Repo.Project().ReadProject(r.ProjectID)
  88. if err != nil {
  89. return nil, fmt.Errorf("error getting project for repository: %w", err)
  90. }
  91. if project.CapiProvisionerEnabled {
  92. uri := strings.TrimPrefix(r.URL, "https://")
  93. splits := strings.Split(uri, ".")
  94. accountID := splits[0]
  95. region := splits[3]
  96. req := connect.NewRequest(&porterv1.AssumeRoleCredentialsRequest{
  97. ProjectId: int64(r.ProjectID),
  98. AwsAccountId: accountID,
  99. })
  100. creds, err := conf.ClusterControlPlaneClient.AssumeRoleCredentials(ctx, req)
  101. if err != nil {
  102. return nil, fmt.Errorf("error getting capi credentials for repository: %w", err)
  103. }
  104. aws := &ints.AWSIntegration{
  105. AWSAccessKeyID: []byte(creds.Msg.AwsAccessId),
  106. AWSSecretAccessKey: []byte(creds.Msg.AwsSecretKey),
  107. AWSSessionToken: []byte(creds.Msg.AwsSessionToken),
  108. AWSRegion: region,
  109. }
  110. return r.listECRRepositories(aws)
  111. }
  112. return nil, fmt.Errorf("error listing repositories")
  113. }
  114. type gcrJWT struct {
  115. AccessToken string `json:"token"`
  116. ExpiresInSec int `json:"expires_in"`
  117. }
  118. type gcrErr struct {
  119. Code string `json:"code"`
  120. Message string `json:"message"`
  121. }
  122. type gcrRepositoryResp struct {
  123. Repositories []string `json:"repositories"`
  124. Errors []gcrErr `json:"errors"`
  125. }
  126. func (r *Registry) GetGCRToken(repo repository.Repository) (*oauth2.Token, error) {
  127. getTokenCache := r.getTokenCacheFunc(repo)
  128. gcp, err := repo.GCPIntegration().ReadGCPIntegration(
  129. r.ProjectID,
  130. r.GCPIntegrationID,
  131. )
  132. if err != nil {
  133. return nil, err
  134. }
  135. // get oauth2 access token
  136. return gcp.GetBearerToken(
  137. getTokenCache,
  138. r.setTokenCacheFunc(repo),
  139. "https://www.googleapis.com/auth/devstorage.read_write",
  140. )
  141. }
  142. func (r *Registry) listGCRRepositories(
  143. repo repository.Repository,
  144. ) ([]*ptypes.RegistryRepository, error) {
  145. gcp, err := repo.GCPIntegration().ReadGCPIntegration(
  146. r.ProjectID,
  147. r.GCPIntegrationID,
  148. )
  149. if err != nil {
  150. return nil, err
  151. }
  152. // Just use service account key to authenticate, since scopes may not be in place
  153. // for oauth. This also prevents us from making more requests.
  154. client := &http.Client{}
  155. regURL := r.URL
  156. if !strings.HasPrefix(regURL, "http") {
  157. regURL = fmt.Sprintf("https://%s", regURL)
  158. }
  159. regURLParsed, err := url.Parse(regURL)
  160. regHostname := "gcr.io"
  161. if err == nil {
  162. regHostname = regURLParsed.Host
  163. }
  164. req, err := http.NewRequest(
  165. "GET",
  166. fmt.Sprintf("https://%s/v2/_catalog", regHostname),
  167. nil,
  168. )
  169. if err != nil {
  170. return nil, err
  171. }
  172. req.SetBasicAuth("_json_key", string(gcp.GCPKeyData))
  173. resp, err := client.Do(req)
  174. if err != nil {
  175. return nil, err
  176. }
  177. gcrResp := gcrRepositoryResp{}
  178. if err := json.NewDecoder(resp.Body).Decode(&gcrResp); err != nil {
  179. return nil, fmt.Errorf("Could not read GCR repositories: %v", err)
  180. }
  181. if len(gcrResp.Errors) > 0 {
  182. errMsg := ""
  183. for _, gcrErr := range gcrResp.Errors {
  184. errMsg += fmt.Sprintf(": Code %s, message %s", gcrErr.Code, gcrErr.Message)
  185. }
  186. return nil, fmt.Errorf(errMsg)
  187. }
  188. res := make([]*ptypes.RegistryRepository, 0)
  189. parsedURL, err := url.Parse("https://" + r.URL)
  190. if err != nil {
  191. return nil, err
  192. }
  193. for _, repo := range gcrResp.Repositories {
  194. res = append(res, &ptypes.RegistryRepository{
  195. Name: repo,
  196. URI: parsedURL.Host + "/" + repo,
  197. })
  198. }
  199. return res, nil
  200. }
  201. func (r *Registry) GetGARToken(repo repository.Repository) (*oauth2.Token, error) {
  202. getTokenCache := r.getTokenCacheFunc(repo)
  203. gcp, err := repo.GCPIntegration().ReadGCPIntegration(
  204. r.ProjectID,
  205. r.GCPIntegrationID,
  206. )
  207. if err != nil {
  208. return nil, err
  209. }
  210. // get oauth2 access token
  211. return gcp.GetBearerToken(
  212. getTokenCache,
  213. r.setTokenCacheFunc(repo),
  214. "https://www.googleapis.com/auth/cloud-platform",
  215. )
  216. }
  217. type garTokenSource struct {
  218. reg *Registry
  219. repo repository.Repository
  220. }
  221. func (source *garTokenSource) Token() (*oauth2.Token, error) {
  222. return source.reg.GetGARToken(source.repo)
  223. }
  224. // GAR has the concept of a "repository" which is a collection of images, unlike ECR or others
  225. // where a repository is a single image. This function returns the list of fully qualified names
  226. // of GAR images including their repository names.
  227. func (r *Registry) listGARRepositories(
  228. repo repository.Repository,
  229. ) ([]*ptypes.RegistryRepository, error) {
  230. gcpInt, err := repo.GCPIntegration().ReadGCPIntegration(
  231. r.ProjectID,
  232. r.GCPIntegrationID,
  233. )
  234. if err != nil {
  235. return nil, err
  236. }
  237. client, err := artifactregistry.NewClient(context.Background(), option.WithTokenSource(&garTokenSource{
  238. reg: r,
  239. repo: repo,
  240. }), option.WithScopes("roles/artifactregistry.reader"))
  241. if err != nil {
  242. return nil, err
  243. }
  244. var repoNames []string
  245. nextToken := ""
  246. parsedURL, err := url.Parse("https://" + r.URL)
  247. if err != nil {
  248. return nil, err
  249. }
  250. location := strings.TrimSuffix(parsedURL.Host, "-docker.pkg.dev")
  251. for {
  252. it := client.ListRepositories(context.Background(), &artifactregistrypb.ListRepositoriesRequest{
  253. Parent: fmt.Sprintf("projects/%s/locations/%s", gcpInt.GCPProjectID, location),
  254. PageSize: 1000,
  255. PageToken: nextToken,
  256. })
  257. for {
  258. resp, err := it.Next()
  259. if err == iterator.Done {
  260. break
  261. } else if err != nil {
  262. return nil, err
  263. }
  264. if resp.GetFormat() == artifactregistrypb.Repository_DOCKER { // we only care about
  265. repoSlice := strings.Split(resp.GetName(), "/")
  266. repoName := repoSlice[len(repoSlice)-1]
  267. repoNames = append(repoNames, repoName)
  268. }
  269. }
  270. if it.PageInfo().Token == "" {
  271. break
  272. }
  273. nextToken = it.PageInfo().Token
  274. }
  275. svc, err := v1artifactregistry.NewService(context.Background(), option.WithTokenSource(&garTokenSource{
  276. reg: r,
  277. repo: repo,
  278. }), option.WithScopes("roles/artifactregistry.reader"))
  279. if err != nil {
  280. return nil, err
  281. }
  282. nextToken = ""
  283. dockerSvc := v1artifactregistry.NewProjectsLocationsRepositoriesDockerImagesService(svc)
  284. var (
  285. wg sync.WaitGroup
  286. resMap sync.Map
  287. )
  288. for _, repoName := range repoNames {
  289. wg.Add(1)
  290. go func(repoName string) {
  291. defer wg.Done()
  292. for {
  293. resp, err := dockerSvc.List(fmt.Sprintf("projects/%s/locations/%s/repositories/%s",
  294. gcpInt.GCPProjectID, location, repoName)).PageSize(1000).PageToken(nextToken).Do()
  295. if err != nil {
  296. // FIXME: we should report this error using a channel
  297. return
  298. }
  299. for _, image := range resp.DockerImages {
  300. named, err := reference.ParseNamed(image.Uri)
  301. if err != nil {
  302. // let us skip this image becaue it has a malformed URI coming from the GCP API
  303. continue
  304. }
  305. uploadTime, _ := time.Parse(time.RFC3339, image.UploadTime)
  306. resMap.Store(named.Name(), &ptypes.RegistryRepository{
  307. Name: repoName,
  308. URI: named.Name(),
  309. CreatedAt: uploadTime,
  310. })
  311. }
  312. if resp.NextPageToken == "" {
  313. break
  314. }
  315. nextToken = resp.NextPageToken
  316. }
  317. }(repoName)
  318. }
  319. wg.Wait()
  320. var res []*ptypes.RegistryRepository
  321. resMap.Range(func(_, value any) bool {
  322. res = append(res, value.(*ptypes.RegistryRepository))
  323. return true
  324. })
  325. return res, nil
  326. }
  327. func (r *Registry) listECRRepositories(aws *ints.AWSIntegration) ([]*ptypes.RegistryRepository, error) {
  328. sess, err := aws.GetSession()
  329. if err != nil {
  330. return nil, err
  331. }
  332. svc := ecr.New(sess)
  333. resp, err := svc.DescribeRepositories(&ecr.DescribeRepositoriesInput{})
  334. if err != nil {
  335. return nil, err
  336. }
  337. res := make([]*ptypes.RegistryRepository, 0)
  338. for _, repo := range resp.Repositories {
  339. res = append(res, &ptypes.RegistryRepository{
  340. Name: *repo.RepositoryName,
  341. CreatedAt: *repo.CreatedAt,
  342. URI: *repo.RepositoryUri,
  343. })
  344. }
  345. return res, nil
  346. }
  347. func (r *Registry) listACRRepositories(repo repository.Repository) ([]*ptypes.RegistryRepository, error) {
  348. az, err := repo.AzureIntegration().ReadAzureIntegration(
  349. r.ProjectID,
  350. r.AzureIntegrationID,
  351. )
  352. if err != nil {
  353. return nil, err
  354. }
  355. client := &http.Client{}
  356. req, err := http.NewRequest(
  357. "GET",
  358. fmt.Sprintf("%s/v2/_catalog", r.URL),
  359. nil,
  360. )
  361. if err != nil {
  362. return nil, err
  363. }
  364. req.SetBasicAuth(az.AzureClientID, string(az.ServicePrincipalSecret))
  365. resp, err := client.Do(req)
  366. if err != nil {
  367. return nil, err
  368. }
  369. gcrResp := gcrRepositoryResp{}
  370. if err := json.NewDecoder(resp.Body).Decode(&gcrResp); err != nil {
  371. return nil, fmt.Errorf("Could not read Azure registry repositories: %v", err)
  372. }
  373. res := make([]*ptypes.RegistryRepository, 0)
  374. if err != nil {
  375. return nil, err
  376. }
  377. for _, repo := range gcrResp.Repositories {
  378. res = append(res, &ptypes.RegistryRepository{
  379. Name: repo,
  380. URI: strings.TrimPrefix(r.URL, "https://") + "/" + repo,
  381. })
  382. }
  383. return res, nil
  384. }
  385. // Returns the username/password pair for the registry
  386. func (r *Registry) GetACRCredentials(repo repository.Repository) (string, string, error) {
  387. az, err := repo.AzureIntegration().ReadAzureIntegration(
  388. r.ProjectID,
  389. r.AzureIntegrationID,
  390. )
  391. if err != nil {
  392. return "", "", err
  393. }
  394. // if the passwords and name aren't set, generate them
  395. if az.ACRTokenName == "" || len(az.ACRPassword1) == 0 {
  396. az.ACRTokenName = "porter-acr-token"
  397. // create an acr repo token
  398. cred, err := azidentity.NewClientSecretCredential(az.AzureTenantID, az.AzureClientID, string(az.ServicePrincipalSecret), nil)
  399. if err != nil {
  400. return "", "", err
  401. }
  402. scopeMapsClient, err := armcontainerregistry.NewScopeMapsClient(az.AzureSubscriptionID, cred, nil)
  403. if err != nil {
  404. return "", "", err
  405. }
  406. smRes, err := scopeMapsClient.Get(
  407. context.Background(),
  408. az.ACRResourceGroupName,
  409. az.ACRName,
  410. "_repositories_admin",
  411. nil,
  412. )
  413. if err != nil {
  414. return "", "", err
  415. }
  416. tokensClient, err := armcontainerregistry.NewTokensClient(az.AzureSubscriptionID, cred, nil)
  417. if err != nil {
  418. return "", "", err
  419. }
  420. pollerResp, err := tokensClient.BeginCreate(
  421. context.Background(),
  422. az.ACRResourceGroupName,
  423. az.ACRName,
  424. "porter-acr-token",
  425. armcontainerregistry.Token{
  426. Properties: &armcontainerregistry.TokenProperties{
  427. ScopeMapID: smRes.ID,
  428. Status: to.Ptr(armcontainerregistry.TokenStatusEnabled),
  429. },
  430. },
  431. nil,
  432. )
  433. if err != nil {
  434. return "", "", err
  435. }
  436. tokResp, err := pollerResp.PollUntilDone(context.Background(), 2*time.Second)
  437. if err != nil {
  438. return "", "", err
  439. }
  440. registriesClient, err := armcontainerregistry.NewRegistriesClient(az.AzureSubscriptionID, cred, nil)
  441. if err != nil {
  442. return "", "", err
  443. }
  444. poller, err := registriesClient.BeginGenerateCredentials(
  445. context.Background(),
  446. az.ACRResourceGroupName,
  447. az.ACRName,
  448. armcontainerregistry.GenerateCredentialsParameters{
  449. TokenID: tokResp.ID,
  450. },
  451. &armcontainerregistry.RegistriesClientBeginGenerateCredentialsOptions{ResumeToken: ""})
  452. if err != nil {
  453. return "", "", err
  454. }
  455. genCredentialsResp, err := poller.PollUntilDone(context.Background(), 2*time.Second)
  456. if err != nil {
  457. return "", "", err
  458. }
  459. for i, tokPassword := range genCredentialsResp.Passwords {
  460. if i == 0 {
  461. az.ACRPassword1 = []byte(*tokPassword.Value)
  462. } else if i == 1 {
  463. az.ACRPassword2 = []byte(*tokPassword.Value)
  464. }
  465. }
  466. // update the az integration
  467. az, err = repo.AzureIntegration().OverwriteAzureIntegration(
  468. az,
  469. )
  470. if err != nil {
  471. return "", "", err
  472. }
  473. }
  474. return az.ACRTokenName, string(az.ACRPassword1), nil
  475. }
  476. func (r *Registry) listDOCRRepositories(
  477. repo repository.Repository,
  478. doAuth *oauth2.Config,
  479. ) ([]*ptypes.RegistryRepository, error) {
  480. oauthInt, err := repo.OAuthIntegration().ReadOAuthIntegration(
  481. r.ProjectID,
  482. r.DOIntegrationID,
  483. )
  484. if err != nil {
  485. return nil, err
  486. }
  487. tok, _, err := oauth.GetAccessToken(oauthInt.SharedOAuthModel, doAuth, oauth.MakeUpdateOAuthIntegrationTokenFunction(oauthInt, repo))
  488. if err != nil {
  489. return nil, err
  490. }
  491. client := godo.NewFromToken(tok)
  492. urlArr := strings.Split(r.URL, "/")
  493. if len(urlArr) != 2 {
  494. return nil, fmt.Errorf("invalid digital ocean registry url")
  495. }
  496. name := urlArr[1]
  497. repos, _, err := client.Registry.ListRepositories(context.TODO(), name, &godo.ListOptions{})
  498. if err != nil {
  499. return nil, err
  500. }
  501. res := make([]*ptypes.RegistryRepository, 0)
  502. for _, repo := range repos {
  503. res = append(res, &ptypes.RegistryRepository{
  504. Name: repo.Name,
  505. URI: r.URL + "/" + repo.Name,
  506. })
  507. }
  508. return res, nil
  509. }
  510. func (r *Registry) listPrivateRegistryRepositories(
  511. repo repository.Repository,
  512. ) ([]*ptypes.RegistryRepository, error) {
  513. // handle dockerhub different, as it doesn't implement the docker registry http api
  514. if strings.Contains(r.URL, "docker.io") {
  515. // in this case, we just return the single dockerhub repository that's linked
  516. res := make([]*ptypes.RegistryRepository, 0)
  517. res = append(res, &ptypes.RegistryRepository{
  518. Name: strings.Split(r.URL, "docker.io/")[1],
  519. URI: r.URL,
  520. })
  521. return res, nil
  522. }
  523. basic, err := repo.BasicIntegration().ReadBasicIntegration(
  524. r.ProjectID,
  525. r.BasicIntegrationID,
  526. )
  527. if err != nil {
  528. return nil, err
  529. }
  530. // Just use service account key to authenticate, since scopes may not be in place
  531. // for oauth. This also prevents us from making more requests.
  532. client := &http.Client{}
  533. // get the host and scheme to make the request
  534. parsedURL, err := url.Parse(r.URL)
  535. req, err := http.NewRequest(
  536. "GET",
  537. fmt.Sprintf("%s://%s/v2/_catalog", parsedURL.Scheme, parsedURL.Host),
  538. nil,
  539. )
  540. if err != nil {
  541. return nil, err
  542. }
  543. req.SetBasicAuth(string(basic.Username), string(basic.Password))
  544. resp, err := client.Do(req)
  545. if err != nil {
  546. return nil, err
  547. }
  548. // if the status code is 404, fallback to the Docker Hub implementation
  549. if resp.StatusCode == 404 {
  550. req, err := http.NewRequest(
  551. "GET",
  552. fmt.Sprintf("%s/", r.URL),
  553. nil,
  554. )
  555. if err != nil {
  556. return nil, err
  557. }
  558. req.SetBasicAuth(string(basic.Username), string(basic.Password))
  559. resp, err = client.Do(req)
  560. if err != nil {
  561. return nil, err
  562. }
  563. }
  564. gcrResp := gcrRepositoryResp{}
  565. if err := json.NewDecoder(resp.Body).Decode(&gcrResp); err != nil {
  566. return nil, fmt.Errorf("Could not read private registry repositories: %v", err)
  567. }
  568. res := make([]*ptypes.RegistryRepository, 0)
  569. if err != nil {
  570. return nil, err
  571. }
  572. for _, repo := range gcrResp.Repositories {
  573. res = append(res, &ptypes.RegistryRepository{
  574. Name: repo,
  575. URI: parsedURL.Host + "/" + repo,
  576. })
  577. }
  578. return res, nil
  579. }
  580. func (r *Registry) getTokenCacheFunc(
  581. repo repository.Repository,
  582. ) ints.GetTokenCacheFunc {
  583. return func() (tok *ints.TokenCache, err error) {
  584. reg, err := repo.Registry().ReadRegistry(r.ProjectID, r.ID)
  585. if err != nil {
  586. return nil, err
  587. }
  588. return &reg.TokenCache.TokenCache, nil
  589. }
  590. }
  591. func (r *Registry) setTokenCacheFunc(
  592. repo repository.Repository,
  593. ) ints.SetTokenCacheFunc {
  594. return func(token string, expiry time.Time) error {
  595. _, err := repo.Registry().UpdateRegistryTokenCache(
  596. &ints.RegTokenCache{
  597. TokenCache: ints.TokenCache{
  598. Token: []byte(token),
  599. Expiry: expiry,
  600. },
  601. RegistryID: r.ID,
  602. },
  603. )
  604. return err
  605. }
  606. }
  607. // CreateRepository creates a repository for a registry, if needed
  608. // (currently only required for ECR)
  609. func (r *Registry) CreateRepository(
  610. ctx context.Context,
  611. conf *config.Config,
  612. name string,
  613. ) error {
  614. // if aws, create repository
  615. if r.AWSIntegrationID != 0 {
  616. aws, err := conf.Repo.AWSIntegration().ReadAWSIntegration(
  617. r.ProjectID,
  618. r.AWSIntegrationID,
  619. )
  620. if err != nil {
  621. return err
  622. }
  623. return r.createECRRepository(aws, name)
  624. } else if r.GCPIntegrationID != 0 && strings.Contains(r.URL, "pkg.dev") {
  625. return r.createGARRepository(conf.Repo, name)
  626. }
  627. project, err := conf.Repo.Project().ReadProject(r.ProjectID)
  628. if err != nil {
  629. return fmt.Errorf("error getting project for repository: %w", err)
  630. }
  631. if project.CapiProvisionerEnabled {
  632. uri := strings.TrimPrefix(r.URL, "https://")
  633. splits := strings.Split(uri, ".")
  634. accountID := splits[0]
  635. region := splits[3]
  636. req := connect.NewRequest(&porterv1.AssumeRoleCredentialsRequest{
  637. ProjectId: int64(r.ProjectID),
  638. AwsAccountId: accountID,
  639. })
  640. creds, err := conf.ClusterControlPlaneClient.AssumeRoleCredentials(ctx, req)
  641. if err != nil {
  642. return fmt.Errorf("error getting capi credentials for repository: %w", err)
  643. }
  644. aws := &ints.AWSIntegration{
  645. AWSAccessKeyID: []byte(creds.Msg.AwsAccessId),
  646. AWSSecretAccessKey: []byte(creds.Msg.AwsSecretKey),
  647. AWSSessionToken: []byte(creds.Msg.AwsSessionToken),
  648. AWSRegion: region,
  649. }
  650. return r.createECRRepository(aws, name)
  651. }
  652. // otherwise, no-op
  653. return nil
  654. }
  655. func (r *Registry) createECRRepository(
  656. aws *ints.AWSIntegration,
  657. name string,
  658. ) error {
  659. sess, err := aws.GetSession()
  660. if err != nil {
  661. return err
  662. }
  663. svc := ecr.New(sess)
  664. // determine if repository already exists
  665. _, err = svc.DescribeRepositories(&ecr.DescribeRepositoriesInput{
  666. RepositoryNames: []*string{&name},
  667. })
  668. // if the repository was not found, create it
  669. if aerr, ok := err.(awserr.Error); ok && aerr.Code() == ecr.ErrCodeRepositoryNotFoundException {
  670. _, err = svc.CreateRepository(&ecr.CreateRepositoryInput{
  671. RepositoryName: &name,
  672. })
  673. return err
  674. } else if err != nil {
  675. return err
  676. }
  677. return nil
  678. }
  679. func (r *Registry) createGARRepository(
  680. repo repository.Repository,
  681. name string,
  682. ) error {
  683. gcpInt, err := repo.GCPIntegration().ReadGCPIntegration(
  684. r.ProjectID,
  685. r.GCPIntegrationID,
  686. )
  687. if err != nil {
  688. return err
  689. }
  690. client, err := artifactregistry.NewClient(context.Background(), option.WithTokenSource(&garTokenSource{
  691. reg: r,
  692. repo: repo,
  693. }), option.WithScopes("roles/artifactregistry.admin"))
  694. if err != nil {
  695. return err
  696. }
  697. defer client.Close()
  698. parsedURL, err := url.Parse("https://" + r.URL)
  699. if err != nil {
  700. return err
  701. }
  702. location := strings.TrimSuffix(parsedURL.Host, "-docker.pkg.dev")
  703. _, err = client.GetRepository(context.Background(), &artifactregistrypb.GetRepositoryRequest{
  704. Name: fmt.Sprintf("projects/%s/locations/%s/repositories/%s", gcpInt.GCPProjectID, location, name),
  705. })
  706. if err != nil && strings.Contains(err.Error(), "not found") {
  707. // create a new repository
  708. _, err := client.CreateRepository(context.Background(), &artifactregistrypb.CreateRepositoryRequest{
  709. Parent: fmt.Sprintf("projects/%s/locations/%s", gcpInt.GCPProjectID, location),
  710. RepositoryId: name,
  711. Repository: &artifactregistrypb.Repository{
  712. Format: artifactregistrypb.Repository_DOCKER,
  713. },
  714. })
  715. if err != nil {
  716. return err
  717. }
  718. } else if err != nil {
  719. return err
  720. }
  721. return nil
  722. }
  723. // ListImages lists the images for an image repository
  724. func (r *Registry) ListImages(
  725. ctx context.Context,
  726. repoName string,
  727. repo repository.Repository,
  728. conf *config.Config,
  729. ) ([]*ptypes.Image, error) {
  730. // switch on the auth mechanism to get a token
  731. if r.AWSIntegrationID != 0 {
  732. aws, err := repo.AWSIntegration().ReadAWSIntegration(
  733. r.ProjectID,
  734. r.AWSIntegrationID,
  735. )
  736. if err != nil {
  737. return nil, err
  738. }
  739. return r.listECRImages(aws, repoName, repo)
  740. }
  741. if r.AzureIntegrationID != 0 {
  742. return r.listACRImages(repoName, repo)
  743. }
  744. if r.GCPIntegrationID != 0 {
  745. if strings.Contains(r.URL, "pkg.dev") {
  746. return r.listGARImages(repoName, repo)
  747. }
  748. return r.listGCRImages(repoName, repo)
  749. }
  750. if r.DOIntegrationID != 0 {
  751. return r.listDOCRImages(repoName, repo, conf.DOConf)
  752. }
  753. if r.BasicIntegrationID != 0 {
  754. return r.listPrivateRegistryImages(repoName, repo)
  755. }
  756. project, err := conf.Repo.Project().ReadProject(r.ProjectID)
  757. if err != nil {
  758. return nil, fmt.Errorf("error getting project for repository: %w", err)
  759. }
  760. if project.CapiProvisionerEnabled {
  761. uri := strings.TrimPrefix(r.URL, "https://")
  762. splits := strings.Split(uri, ".")
  763. accountID := splits[0]
  764. region := splits[3]
  765. req := connect.NewRequest(&porterv1.AssumeRoleCredentialsRequest{
  766. ProjectId: int64(r.ProjectID),
  767. AwsAccountId: accountID,
  768. })
  769. creds, err := conf.ClusterControlPlaneClient.AssumeRoleCredentials(ctx, req)
  770. if err != nil {
  771. return nil, fmt.Errorf("error getting capi credentials for repository: %w", err)
  772. }
  773. aws := &ints.AWSIntegration{
  774. AWSAccessKeyID: []byte(creds.Msg.AwsAccessId),
  775. AWSSecretAccessKey: []byte(creds.Msg.AwsSecretKey),
  776. AWSSessionToken: []byte(creds.Msg.AwsSessionToken),
  777. AWSRegion: region,
  778. }
  779. return r.listECRImages(aws, repoName, repo)
  780. }
  781. return nil, fmt.Errorf("error listing images")
  782. }
  783. func (r *Registry) GetECRPaginatedImages(
  784. repoName string,
  785. repo repository.Repository,
  786. maxResults int64,
  787. nextToken *string,
  788. ) ([]*ptypes.Image, *string, error) {
  789. aws, err := repo.AWSIntegration().ReadAWSIntegration(
  790. r.ProjectID,
  791. r.AWSIntegrationID,
  792. )
  793. if err != nil {
  794. return nil, nil, err
  795. }
  796. sess, err := aws.GetSession()
  797. if err != nil {
  798. return nil, nil, err
  799. }
  800. svc := ecr.New(sess)
  801. resp, err := svc.ListImages(&ecr.ListImagesInput{
  802. RepositoryName: &repoName,
  803. MaxResults: &maxResults,
  804. NextToken: nextToken,
  805. })
  806. if err != nil {
  807. return nil, nil, err
  808. }
  809. if len(resp.ImageIds) == 0 {
  810. return []*ptypes.Image{}, nil, nil
  811. }
  812. imageIDLen := len(resp.ImageIds)
  813. imageDetails := make([]*ecr.ImageDetail, 0)
  814. imageIDMap := make(map[string]bool)
  815. for _, id := range resp.ImageIds {
  816. if id != nil && id.ImageTag != nil {
  817. imageIDMap[*id.ImageTag] = true
  818. }
  819. }
  820. var wg sync.WaitGroup
  821. var mu sync.Mutex
  822. // AWS API expects the length of imageIDs to be at max 100 at a time
  823. for start := 0; start < imageIDLen; start += 100 {
  824. end := start + 100
  825. if end > imageIDLen {
  826. end = imageIDLen
  827. }
  828. wg.Add(1)
  829. go func(start, end int) {
  830. defer wg.Done()
  831. describeResp, err := svc.DescribeImages(&ecr.DescribeImagesInput{
  832. RepositoryName: &repoName,
  833. ImageIds: resp.ImageIds[start:end],
  834. })
  835. if err != nil {
  836. return
  837. }
  838. mu.Lock()
  839. imageDetails = append(imageDetails, describeResp.ImageDetails...)
  840. mu.Unlock()
  841. }(start, end)
  842. }
  843. wg.Wait()
  844. res := make([]*ptypes.Image, 0)
  845. imageInfoMap := make(map[string]*ptypes.Image)
  846. for _, img := range imageDetails {
  847. for _, tag := range img.ImageTags {
  848. newImage := &ptypes.Image{
  849. Digest: *img.ImageDigest,
  850. Tag: *tag,
  851. RepositoryName: repoName,
  852. PushedAt: img.ImagePushedAt,
  853. }
  854. if _, ok := imageIDMap[*tag]; ok {
  855. if _, ok := imageInfoMap[*tag]; !ok {
  856. imageInfoMap[*tag] = newImage
  857. }
  858. }
  859. if len(imageInfoMap) == int(maxResults) {
  860. break
  861. }
  862. }
  863. if len(imageInfoMap) == int(maxResults) {
  864. break
  865. }
  866. }
  867. for _, v := range imageInfoMap {
  868. res = append(res, v)
  869. }
  870. sort.Slice(res, func(i, j int) bool {
  871. if res[i].PushedAt == nil || res[j].PushedAt == nil {
  872. return false
  873. }
  874. return res[i].PushedAt.After(*res[j].PushedAt)
  875. })
  876. return res, resp.NextToken, nil
  877. }
  878. func (r *Registry) listECRImages(aws *ints.AWSIntegration, repoName string, repo repository.Repository) ([]*ptypes.Image, error) {
  879. sess, err := aws.GetSession()
  880. if err != nil {
  881. return nil, err
  882. }
  883. svc := ecr.New(sess)
  884. maxResults := int64(1000)
  885. var imageIDs []*ecr.ImageIdentifier
  886. resp, err := svc.ListImages(&ecr.ListImagesInput{
  887. RepositoryName: &repoName,
  888. MaxResults: &maxResults,
  889. })
  890. if err != nil {
  891. return nil, err
  892. }
  893. if len(resp.ImageIds) == 0 {
  894. return []*ptypes.Image{}, nil
  895. }
  896. imageIDs = append(imageIDs, resp.ImageIds...)
  897. nextToken := resp.NextToken
  898. for nextToken != nil {
  899. resp, err := svc.ListImages(&ecr.ListImagesInput{
  900. RepositoryName: &repoName,
  901. MaxResults: &maxResults,
  902. NextToken: nextToken,
  903. })
  904. if err != nil {
  905. return nil, err
  906. }
  907. imageIDs = append(imageIDs, resp.ImageIds...)
  908. nextToken = resp.NextToken
  909. }
  910. imageIDLen := len(imageIDs)
  911. imageDetails := make([]*ecr.ImageDetail, 0)
  912. var wg sync.WaitGroup
  913. var mu sync.Mutex
  914. // AWS API expects the length of imageIDs to be at max 100 at a time
  915. for start := 0; start < imageIDLen; start += 100 {
  916. end := start + 100
  917. if end > imageIDLen {
  918. end = imageIDLen
  919. }
  920. wg.Add(1)
  921. go func(start, end int) {
  922. defer wg.Done()
  923. describeResp, err := svc.DescribeImages(&ecr.DescribeImagesInput{
  924. RepositoryName: &repoName,
  925. ImageIds: imageIDs[start:end],
  926. })
  927. if err != nil {
  928. return
  929. }
  930. mu.Lock()
  931. imageDetails = append(imageDetails, describeResp.ImageDetails...)
  932. mu.Unlock()
  933. }(start, end)
  934. }
  935. wg.Wait()
  936. res := make([]*ptypes.Image, 0)
  937. imageInfoMap := make(map[string]*ptypes.Image)
  938. for _, img := range imageDetails {
  939. for _, tag := range img.ImageTags {
  940. newImage := &ptypes.Image{
  941. Digest: *img.ImageDigest,
  942. Tag: *tag,
  943. RepositoryName: repoName,
  944. PushedAt: img.ImagePushedAt,
  945. }
  946. if _, ok := imageInfoMap[*tag]; !ok {
  947. imageInfoMap[*tag] = newImage
  948. }
  949. }
  950. }
  951. for _, v := range imageInfoMap {
  952. res = append(res, v)
  953. }
  954. return res, nil
  955. }
  956. func (r *Registry) listACRImages(repoName string, repo repository.Repository) ([]*ptypes.Image, error) {
  957. az, err := repo.AzureIntegration().ReadAzureIntegration(
  958. r.ProjectID,
  959. r.AzureIntegrationID,
  960. )
  961. if err != nil {
  962. return nil, err
  963. }
  964. // use JWT token to request catalog
  965. client := &http.Client{}
  966. req, err := http.NewRequest(
  967. "GET",
  968. fmt.Sprintf("%s/v2/%s/tags/list", r.URL, repoName),
  969. nil,
  970. )
  971. if err != nil {
  972. return nil, err
  973. }
  974. req.SetBasicAuth(az.AzureClientID, string(az.ServicePrincipalSecret))
  975. resp, err := client.Do(req)
  976. if err != nil {
  977. return nil, err
  978. }
  979. gcrResp := gcrImageResp{}
  980. if err := json.NewDecoder(resp.Body).Decode(&gcrResp); err != nil {
  981. return nil, fmt.Errorf("Could not read GCR repositories: %v", err)
  982. }
  983. res := make([]*ptypes.Image, 0)
  984. for _, tag := range gcrResp.Tags {
  985. res = append(res, &ptypes.Image{
  986. RepositoryName: strings.TrimPrefix(repoName, "https://"),
  987. Tag: tag,
  988. })
  989. }
  990. return res, nil
  991. }
  992. type gcrImageResp struct {
  993. Tags []string `json:"tags"`
  994. }
  995. func (r *Registry) listGCRImages(repoName string, repo repository.Repository) ([]*ptypes.Image, error) {
  996. gcp, err := repo.GCPIntegration().ReadGCPIntegration(
  997. r.ProjectID,
  998. r.GCPIntegrationID,
  999. )
  1000. if err != nil {
  1001. return nil, err
  1002. }
  1003. // use JWT token to request catalog
  1004. client := &http.Client{}
  1005. parsedURL, err := url.Parse("https://" + r.URL)
  1006. if err != nil {
  1007. return nil, err
  1008. }
  1009. trimmedPath := strings.Trim(parsedURL.Path, "/")
  1010. req, err := http.NewRequest(
  1011. "GET",
  1012. fmt.Sprintf("https://%s/v2/%s/%s/tags/list", parsedURL.Host, trimmedPath, repoName),
  1013. nil,
  1014. )
  1015. if err != nil {
  1016. return nil, err
  1017. }
  1018. req.SetBasicAuth("_json_key", string(gcp.GCPKeyData))
  1019. resp, err := client.Do(req)
  1020. if err != nil {
  1021. return nil, err
  1022. }
  1023. gcrResp := gcrImageResp{}
  1024. if err := json.NewDecoder(resp.Body).Decode(&gcrResp); err != nil {
  1025. return nil, fmt.Errorf("Could not read GCR repositories: %v", err)
  1026. }
  1027. res := make([]*ptypes.Image, 0)
  1028. for _, tag := range gcrResp.Tags {
  1029. res = append(res, &ptypes.Image{
  1030. RepositoryName: repoName,
  1031. Tag: tag,
  1032. })
  1033. }
  1034. return res, nil
  1035. }
  1036. func (r *Registry) listGARImages(repoName string, repo repository.Repository) ([]*ptypes.Image, error) {
  1037. repoImageSlice := strings.Split(repoName, "/")
  1038. if len(repoImageSlice) != 2 {
  1039. return nil, fmt.Errorf("invalid GAR repo name: %s. Expected to be in the form of REPOSITORY/IMAGE", repoName)
  1040. }
  1041. gcpInt, err := repo.GCPIntegration().ReadGCPIntegration(
  1042. r.ProjectID,
  1043. r.GCPIntegrationID,
  1044. )
  1045. if err != nil {
  1046. return nil, err
  1047. }
  1048. svc, err := v1artifactregistry.NewService(context.Background(), option.WithTokenSource(&garTokenSource{
  1049. reg: r,
  1050. repo: repo,
  1051. }), option.WithScopes("roles/artifactregistry.reader"))
  1052. if err != nil {
  1053. return nil, err
  1054. }
  1055. var res []*ptypes.Image
  1056. parsedURL, err := url.Parse("https://" + r.URL)
  1057. if err != nil {
  1058. return nil, err
  1059. }
  1060. location := strings.TrimSuffix(parsedURL.Host, "-docker.pkg.dev")
  1061. dockerSvc := v1artifactregistry.NewProjectsLocationsRepositoriesDockerImagesService(svc)
  1062. nextToken := ""
  1063. for {
  1064. resp, err := dockerSvc.List(fmt.Sprintf("projects/%s/locations/%s/repositories/%s",
  1065. gcpInt.GCPProjectID, location, repoImageSlice[0])).PageSize(1000).PageToken(nextToken).Do()
  1066. if err != nil {
  1067. return nil, err
  1068. }
  1069. for _, image := range resp.DockerImages {
  1070. named, err := reference.ParseNamed(image.Uri)
  1071. if err != nil {
  1072. continue
  1073. }
  1074. paths := strings.Split(reference.Path(named), "/")
  1075. imageName := paths[len(paths)-1]
  1076. if imageName == repoImageSlice[1] {
  1077. uploadTime, _ := time.Parse(time.RFC3339, image.UploadTime)
  1078. for _, tag := range image.Tags {
  1079. res = append(res, &ptypes.Image{
  1080. RepositoryName: repoName,
  1081. Tag: tag,
  1082. PushedAt: &uploadTime,
  1083. Digest: strings.Split(image.Uri, "@")[1],
  1084. })
  1085. }
  1086. }
  1087. }
  1088. if resp.NextPageToken == "" {
  1089. break
  1090. }
  1091. nextToken = resp.NextPageToken
  1092. }
  1093. return res, nil
  1094. }
  1095. func (r *Registry) listDOCRImages(
  1096. repoName string,
  1097. repo repository.Repository,
  1098. doAuth *oauth2.Config,
  1099. ) ([]*ptypes.Image, error) {
  1100. oauthInt, err := repo.OAuthIntegration().ReadOAuthIntegration(
  1101. r.ProjectID,
  1102. r.DOIntegrationID,
  1103. )
  1104. if err != nil {
  1105. return nil, err
  1106. }
  1107. tok, _, err := oauth.GetAccessToken(oauthInt.SharedOAuthModel, doAuth, oauth.MakeUpdateOAuthIntegrationTokenFunction(oauthInt, repo))
  1108. if err != nil {
  1109. return nil, err
  1110. }
  1111. client := godo.NewFromToken(tok)
  1112. urlArr := strings.Split(r.URL, "/")
  1113. if len(urlArr) != 2 {
  1114. return nil, fmt.Errorf("invalid digital ocean registry url")
  1115. }
  1116. name := urlArr[1]
  1117. var tags []*godo.RepositoryTag
  1118. opt := &godo.ListOptions{
  1119. PerPage: 200,
  1120. }
  1121. for {
  1122. nextTags, resp, err := client.Registry.ListRepositoryTags(context.TODO(), name, repoName, opt)
  1123. if err != nil {
  1124. return nil, err
  1125. }
  1126. tags = append(tags, nextTags...)
  1127. if resp.Links == nil || resp.Links.IsLastPage() {
  1128. break
  1129. }
  1130. page, err := resp.Links.CurrentPage()
  1131. if err != nil {
  1132. return nil, err
  1133. }
  1134. opt.Page = page + 1
  1135. }
  1136. res := make([]*ptypes.Image, 0)
  1137. for _, tag := range tags {
  1138. res = append(res, &ptypes.Image{
  1139. RepositoryName: repoName,
  1140. Tag: tag.Tag,
  1141. })
  1142. }
  1143. return res, nil
  1144. }
  1145. func (r *Registry) listPrivateRegistryImages(repoName string, repo repository.Repository) ([]*ptypes.Image, error) {
  1146. // handle dockerhub different, as it doesn't implement the docker registry http api
  1147. if strings.Contains(r.URL, "docker.io") {
  1148. return r.listDockerHubImages(repoName, repo)
  1149. }
  1150. basic, err := repo.BasicIntegration().ReadBasicIntegration(
  1151. r.ProjectID,
  1152. r.BasicIntegrationID,
  1153. )
  1154. if err != nil {
  1155. return nil, err
  1156. }
  1157. // Just use service account key to authenticate, since scopes may not be in place
  1158. // for oauth. This also prevents us from making more requests.
  1159. client := &http.Client{}
  1160. // get the host and scheme to make the request
  1161. parsedURL, err := url.Parse(r.URL)
  1162. req, err := http.NewRequest(
  1163. "GET",
  1164. fmt.Sprintf("%s://%s/v2/%s/tags/list", parsedURL.Scheme, parsedURL.Host, repoName),
  1165. nil,
  1166. )
  1167. if err != nil {
  1168. return nil, err
  1169. }
  1170. req.SetBasicAuth(string(basic.Username), string(basic.Password))
  1171. resp, err := client.Do(req)
  1172. if err != nil {
  1173. return nil, err
  1174. }
  1175. gcrResp := gcrImageResp{}
  1176. if err := json.NewDecoder(resp.Body).Decode(&gcrResp); err != nil {
  1177. return nil, fmt.Errorf("Could not read private registry repositories: %v", err)
  1178. }
  1179. res := make([]*ptypes.Image, 0)
  1180. for _, tag := range gcrResp.Tags {
  1181. res = append(res, &ptypes.Image{
  1182. RepositoryName: repoName,
  1183. Tag: tag,
  1184. })
  1185. }
  1186. return res, nil
  1187. }
  1188. type dockerHubImageResult struct {
  1189. Name string `json:"name"`
  1190. }
  1191. type dockerHubImageResp struct {
  1192. Results []dockerHubImageResult `json:"results"`
  1193. }
  1194. type dockerHubLoginReq struct {
  1195. Username string `json:"username"`
  1196. Password string `json:"password"`
  1197. }
  1198. type dockerHubLoginResp struct {
  1199. Token string `json:"token"`
  1200. }
  1201. func (r *Registry) listDockerHubImages(repoName string, repo repository.Repository) ([]*ptypes.Image, error) {
  1202. basic, err := repo.BasicIntegration().ReadBasicIntegration(
  1203. r.ProjectID,
  1204. r.BasicIntegrationID,
  1205. )
  1206. if err != nil {
  1207. return nil, err
  1208. }
  1209. client := &http.Client{}
  1210. // first, make a request for the access token
  1211. data, err := json.Marshal(&dockerHubLoginReq{
  1212. Username: string(basic.Username),
  1213. Password: string(basic.Password),
  1214. })
  1215. if err != nil {
  1216. return nil, err
  1217. }
  1218. req, err := http.NewRequest(
  1219. "POST",
  1220. "https://hub.docker.com/v2/users/login",
  1221. strings.NewReader(string(data)),
  1222. )
  1223. if err != nil {
  1224. return nil, err
  1225. }
  1226. req.Header.Add("Content-Type", "application/json")
  1227. resp, err := client.Do(req)
  1228. if err != nil {
  1229. return nil, err
  1230. }
  1231. tokenObj := dockerHubLoginResp{}
  1232. if err := json.NewDecoder(resp.Body).Decode(&tokenObj); err != nil {
  1233. return nil, fmt.Errorf("Could not decode Dockerhub token from response: %v", err)
  1234. }
  1235. req, err = http.NewRequest(
  1236. "GET",
  1237. fmt.Sprintf("https://hub.docker.com/v2/repositories/%s/tags", strings.Split(r.URL, "docker.io/")[1]),
  1238. nil,
  1239. )
  1240. if err != nil {
  1241. return nil, err
  1242. }
  1243. req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", tokenObj.Token))
  1244. resp, err = client.Do(req)
  1245. if err != nil {
  1246. return nil, err
  1247. }
  1248. imageResp := dockerHubImageResp{}
  1249. if err := json.NewDecoder(resp.Body).Decode(&imageResp); err != nil {
  1250. return nil, fmt.Errorf("Could not read private registry repositories: %v", err)
  1251. }
  1252. res := make([]*ptypes.Image, 0)
  1253. for _, result := range imageResp.Results {
  1254. res = append(res, &ptypes.Image{
  1255. RepositoryName: repoName,
  1256. Tag: result.Name,
  1257. })
  1258. }
  1259. return res, nil
  1260. }
  1261. // GetDockerConfigJSON returns a dockerconfigjson file contents with "auths"
  1262. // populated.
  1263. func (r *Registry) GetDockerConfigJSON(
  1264. repo repository.Repository,
  1265. doAuth *oauth2.Config, // only required if using DOCR
  1266. ) ([]byte, error) {
  1267. var conf *configfile.ConfigFile
  1268. var err error
  1269. // switch on the auth mechanism to get a token
  1270. if r.AWSIntegrationID != 0 {
  1271. conf, err = r.getECRDockerConfigFile(repo)
  1272. }
  1273. if r.GCPIntegrationID != 0 {
  1274. conf, err = r.getGCRDockerConfigFile(repo)
  1275. }
  1276. if r.DOIntegrationID != 0 {
  1277. conf, err = r.getDOCRDockerConfigFile(repo, doAuth)
  1278. }
  1279. if r.BasicIntegrationID != 0 {
  1280. conf, err = r.getPrivateRegistryDockerConfigFile(repo)
  1281. }
  1282. if r.AzureIntegrationID != 0 {
  1283. conf, err = r.getACRDockerConfigFile(repo)
  1284. }
  1285. if err != nil {
  1286. return nil, err
  1287. }
  1288. return json.Marshal(conf)
  1289. }
  1290. func (r *Registry) getECRDockerConfigFile(
  1291. repo repository.Repository,
  1292. ) (*configfile.ConfigFile, error) {
  1293. aws, err := repo.AWSIntegration().ReadAWSIntegration(
  1294. r.ProjectID,
  1295. r.AWSIntegrationID,
  1296. )
  1297. if err != nil {
  1298. return nil, err
  1299. }
  1300. sess, err := aws.GetSession()
  1301. if err != nil {
  1302. return nil, err
  1303. }
  1304. ecrSvc := ecr.New(sess)
  1305. output, err := ecrSvc.GetAuthorizationToken(&ecr.GetAuthorizationTokenInput{})
  1306. if err != nil {
  1307. return nil, err
  1308. }
  1309. token := *output.AuthorizationData[0].AuthorizationToken
  1310. decodedToken, err := base64.StdEncoding.DecodeString(token)
  1311. if err != nil {
  1312. return nil, err
  1313. }
  1314. parts := strings.SplitN(string(decodedToken), ":", 2)
  1315. if len(parts) < 2 {
  1316. return nil, err
  1317. }
  1318. key := r.URL
  1319. if !strings.Contains(key, "http") {
  1320. key = "https://" + key
  1321. }
  1322. return &configfile.ConfigFile{
  1323. AuthConfigs: map[string]types.AuthConfig{
  1324. key: {
  1325. Username: parts[0],
  1326. Password: parts[1],
  1327. Auth: token,
  1328. },
  1329. },
  1330. }, nil
  1331. }
  1332. func (r *Registry) getGCRDockerConfigFile(
  1333. repo repository.Repository,
  1334. ) (*configfile.ConfigFile, error) {
  1335. gcp, err := repo.GCPIntegration().ReadGCPIntegration(
  1336. r.ProjectID,
  1337. r.GCPIntegrationID,
  1338. )
  1339. if err != nil {
  1340. return nil, err
  1341. }
  1342. key := r.URL
  1343. if !strings.Contains(key, "http") {
  1344. key = "https://" + key
  1345. }
  1346. parsedURL, _ := url.Parse(key)
  1347. return &configfile.ConfigFile{
  1348. AuthConfigs: map[string]types.AuthConfig{
  1349. parsedURL.Host: {
  1350. Username: "_json_key",
  1351. Password: string(gcp.GCPKeyData),
  1352. Auth: generateAuthToken("_json_key", string(gcp.GCPKeyData)),
  1353. },
  1354. },
  1355. }, nil
  1356. }
  1357. func (r *Registry) getDOCRDockerConfigFile(
  1358. repo repository.Repository,
  1359. doAuth *oauth2.Config,
  1360. ) (*configfile.ConfigFile, error) {
  1361. oauthInt, err := repo.OAuthIntegration().ReadOAuthIntegration(
  1362. r.ProjectID,
  1363. r.DOIntegrationID,
  1364. )
  1365. if err != nil {
  1366. return nil, err
  1367. }
  1368. tok, _, err := oauth.GetAccessToken(oauthInt.SharedOAuthModel, doAuth, oauth.MakeUpdateOAuthIntegrationTokenFunction(oauthInt, repo))
  1369. if err != nil {
  1370. return nil, err
  1371. }
  1372. key := r.URL
  1373. if !strings.Contains(key, "http") {
  1374. key = "https://" + key
  1375. }
  1376. parsedURL, _ := url.Parse(key)
  1377. return &configfile.ConfigFile{
  1378. AuthConfigs: map[string]types.AuthConfig{
  1379. parsedURL.Host: {
  1380. Username: tok,
  1381. Password: tok,
  1382. Auth: generateAuthToken(tok, tok),
  1383. },
  1384. },
  1385. }, nil
  1386. }
  1387. func (r *Registry) getPrivateRegistryDockerConfigFile(
  1388. repo repository.Repository,
  1389. ) (*configfile.ConfigFile, error) {
  1390. basic, err := repo.BasicIntegration().ReadBasicIntegration(
  1391. r.ProjectID,
  1392. r.BasicIntegrationID,
  1393. )
  1394. if err != nil {
  1395. return nil, err
  1396. }
  1397. key := r.URL
  1398. if !strings.Contains(key, "http") {
  1399. key = "https://" + key
  1400. }
  1401. parsedURL, _ := url.Parse(key)
  1402. authConfigKey := parsedURL.Host
  1403. if strings.Contains(r.URL, "index.docker.io") {
  1404. authConfigKey = "https://index.docker.io/v1/"
  1405. }
  1406. return &configfile.ConfigFile{
  1407. AuthConfigs: map[string]types.AuthConfig{
  1408. authConfigKey: {
  1409. Username: string(basic.Username),
  1410. Password: string(basic.Password),
  1411. Auth: generateAuthToken(string(basic.Username), string(basic.Password)),
  1412. },
  1413. },
  1414. }, nil
  1415. }
  1416. func (r *Registry) getACRDockerConfigFile(
  1417. repo repository.Repository,
  1418. ) (*configfile.ConfigFile, error) {
  1419. username, pw, err := r.GetACRCredentials(repo)
  1420. if err != nil {
  1421. return nil, err
  1422. }
  1423. key := r.URL
  1424. if !strings.Contains(key, "http") {
  1425. key = "https://" + key
  1426. }
  1427. parsedURL, _ := url.Parse(key)
  1428. return &configfile.ConfigFile{
  1429. AuthConfigs: map[string]types.AuthConfig{
  1430. parsedURL.Host: {
  1431. Username: string(username),
  1432. Password: string(pw),
  1433. Auth: generateAuthToken(string(username), string(pw)),
  1434. },
  1435. },
  1436. }, nil
  1437. }
  1438. func generateAuthToken(username, password string) string {
  1439. return base64.StdEncoding.EncodeToString([]byte(username + ":" + password))
  1440. }