registry.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601
  1. package registry
  2. import (
  3. "context"
  4. "encoding/base64"
  5. "encoding/json"
  6. "fmt"
  7. "net/http"
  8. "net/url"
  9. "strings"
  10. "time"
  11. "github.com/aws/aws-sdk-go/service/ecr"
  12. "github.com/porter-dev/porter/internal/models"
  13. "github.com/porter-dev/porter/internal/oauth"
  14. "github.com/porter-dev/porter/internal/repository"
  15. "golang.org/x/oauth2"
  16. ints "github.com/porter-dev/porter/internal/models/integrations"
  17. "github.com/digitalocean/godo"
  18. "github.com/docker/cli/cli/config/configfile"
  19. "github.com/docker/cli/cli/config/types"
  20. )
  21. // Registry wraps the gorm Registry model
  22. type Registry models.Registry
  23. // Repository is a collection of images
  24. type Repository struct {
  25. // Name of the repository
  26. Name string `json:"name"`
  27. // When the repository was created
  28. CreatedAt time.Time `json:"created_at,omitempty"`
  29. // The URI of the repository
  30. URI string `json:"uri"`
  31. }
  32. // Image is a Docker image type
  33. type Image struct {
  34. // The sha256 digest of the image manifest.
  35. Digest string `json:"digest"`
  36. // The tag used for the image.
  37. Tag string `json:"tag"`
  38. // The image manifest associated with the image.
  39. Manifest string `json:"manifest"`
  40. // The name of the repository associated with the image.
  41. RepositoryName string `json:"repository_name"`
  42. }
  43. // ListRepositories lists the repositories for a registry
  44. func (r *Registry) ListRepositories(
  45. repo repository.Repository,
  46. doAuth *oauth2.Config, // only required if using DOCR
  47. ) ([]*Repository, error) {
  48. // switch on the auth mechanism to get a token
  49. if r.AWSIntegrationID != 0 {
  50. return r.listECRRepositories(repo)
  51. }
  52. if r.GCPIntegrationID != 0 {
  53. return r.listGCRRepositories(repo)
  54. }
  55. if r.DOIntegrationID != 0 {
  56. return r.listDOCRRepositories(repo, doAuth)
  57. }
  58. return nil, fmt.Errorf("error listing repositories")
  59. }
  60. type gcrJWT struct {
  61. AccessToken string `json:"token"`
  62. ExpiresInSec int `json:"expires_in"`
  63. }
  64. type gcrRepositoryResp struct {
  65. Repositories []string `json:"repositories"`
  66. }
  67. func (r *Registry) GetGCRToken(repo repository.Repository) (*ints.TokenCache, error) {
  68. gcp, err := repo.GCPIntegration.ReadGCPIntegration(
  69. r.GCPIntegrationID,
  70. )
  71. if err != nil {
  72. return nil, err
  73. }
  74. // get oauth2 access token
  75. _, err = gcp.GetBearerToken(
  76. r.getTokenCache,
  77. r.setTokenCacheFunc(repo),
  78. "https://www.googleapis.com/auth/devstorage.read_write",
  79. )
  80. if err != nil {
  81. return nil, err
  82. }
  83. // it's now written to the token cache, so return
  84. cache, err := r.getTokenCache()
  85. if err != nil {
  86. return nil, err
  87. }
  88. return cache, nil
  89. }
  90. func (r *Registry) listGCRRepositories(
  91. repo repository.Repository,
  92. ) ([]*Repository, error) {
  93. gcp, err := repo.GCPIntegration.ReadGCPIntegration(
  94. r.GCPIntegrationID,
  95. )
  96. if err != nil {
  97. return nil, err
  98. }
  99. // Just use service account key to authenticate, since scopes may not be in place
  100. // for oauth. This also prevents us from making more requests.
  101. client := &http.Client{}
  102. req, err := http.NewRequest(
  103. "GET",
  104. "https://gcr.io/v2/_catalog",
  105. nil,
  106. )
  107. if err != nil {
  108. return nil, err
  109. }
  110. req.SetBasicAuth("_json_key", string(gcp.GCPKeyData))
  111. resp, err := client.Do(req)
  112. if err != nil {
  113. return nil, err
  114. }
  115. gcrResp := gcrRepositoryResp{}
  116. if err := json.NewDecoder(resp.Body).Decode(&gcrResp); err != nil {
  117. return nil, fmt.Errorf("Could not read GCR repositories: %v", err)
  118. }
  119. res := make([]*Repository, 0)
  120. parsedURL, err := url.Parse("https://" + r.URL)
  121. if err != nil {
  122. return nil, err
  123. }
  124. for _, repo := range gcrResp.Repositories {
  125. res = append(res, &Repository{
  126. Name: repo,
  127. URI: parsedURL.Host + "/" + repo,
  128. })
  129. }
  130. return res, nil
  131. }
  132. func (r *Registry) listECRRepositories(repo repository.Repository) ([]*Repository, error) {
  133. aws, err := repo.AWSIntegration.ReadAWSIntegration(
  134. r.AWSIntegrationID,
  135. )
  136. if err != nil {
  137. return nil, err
  138. }
  139. sess, err := aws.GetSession()
  140. if err != nil {
  141. return nil, err
  142. }
  143. svc := ecr.New(sess)
  144. resp, err := svc.DescribeRepositories(&ecr.DescribeRepositoriesInput{})
  145. if err != nil {
  146. return nil, err
  147. }
  148. res := make([]*Repository, 0)
  149. for _, repo := range resp.Repositories {
  150. res = append(res, &Repository{
  151. Name: *repo.RepositoryName,
  152. CreatedAt: *repo.CreatedAt,
  153. URI: *repo.RepositoryUri,
  154. })
  155. }
  156. return res, nil
  157. }
  158. func (r *Registry) listDOCRRepositories(
  159. repo repository.Repository,
  160. doAuth *oauth2.Config,
  161. ) ([]*Repository, error) {
  162. oauthInt, err := repo.OAuthIntegration.ReadOAuthIntegration(
  163. r.DOIntegrationID,
  164. )
  165. if err != nil {
  166. return nil, err
  167. }
  168. tok, _, err := oauth.GetAccessToken(oauthInt, doAuth, repo)
  169. if err != nil {
  170. return nil, err
  171. }
  172. client := godo.NewFromToken(tok)
  173. urlArr := strings.Split(r.URL, "/")
  174. if len(urlArr) != 2 {
  175. return nil, fmt.Errorf("invalid digital ocean registry url")
  176. }
  177. name := urlArr[1]
  178. repos, _, err := client.Registry.ListRepositories(context.TODO(), name, &godo.ListOptions{})
  179. if err != nil {
  180. return nil, err
  181. }
  182. res := make([]*Repository, 0)
  183. for _, repo := range repos {
  184. res = append(res, &Repository{
  185. Name: repo.Name,
  186. URI: r.URL + "/" + repo.Name,
  187. })
  188. }
  189. return res, nil
  190. }
  191. func (r *Registry) getTokenCache() (tok *ints.TokenCache, err error) {
  192. return &ints.TokenCache{
  193. Token: r.TokenCache.Token,
  194. Expiry: r.TokenCache.Expiry,
  195. }, nil
  196. }
  197. func (r *Registry) setTokenCacheFunc(
  198. repo repository.Repository,
  199. ) ints.SetTokenCacheFunc {
  200. return func(token string, expiry time.Time) error {
  201. _, err := repo.Registry.UpdateRegistryTokenCache(
  202. &ints.RegTokenCache{
  203. TokenCache: ints.TokenCache{
  204. Token: []byte(token),
  205. Expiry: expiry,
  206. },
  207. RegistryID: r.ID,
  208. },
  209. )
  210. return err
  211. }
  212. }
  213. // ListImages lists the images for an image repository
  214. func (r *Registry) ListImages(
  215. repoName string,
  216. repo repository.Repository,
  217. doAuth *oauth2.Config, // only required if using DOCR
  218. ) ([]*Image, error) {
  219. // switch on the auth mechanism to get a token
  220. if r.AWSIntegrationID != 0 {
  221. return r.listECRImages(repoName, repo)
  222. }
  223. if r.GCPIntegrationID != 0 {
  224. return r.listGCRImages(repoName, repo)
  225. }
  226. if r.DOIntegrationID != 0 {
  227. return r.listDOCRImages(repoName, repo, doAuth)
  228. }
  229. return nil, fmt.Errorf("error listing images")
  230. }
  231. func (r *Registry) listECRImages(repoName string, repo repository.Repository) ([]*Image, error) {
  232. aws, err := repo.AWSIntegration.ReadAWSIntegration(
  233. r.AWSIntegrationID,
  234. )
  235. if err != nil {
  236. return nil, err
  237. }
  238. sess, err := aws.GetSession()
  239. if err != nil {
  240. return nil, err
  241. }
  242. svc := ecr.New(sess)
  243. resp, err := svc.ListImages(&ecr.ListImagesInput{
  244. RepositoryName: &repoName,
  245. })
  246. if err != nil {
  247. return nil, err
  248. }
  249. res := make([]*Image, 0)
  250. for _, img := range resp.ImageIds {
  251. res = append(res, &Image{
  252. Digest: *img.ImageDigest,
  253. Tag: *img.ImageTag,
  254. RepositoryName: repoName,
  255. })
  256. }
  257. return res, nil
  258. }
  259. type gcrImageResp struct {
  260. Tags []string `json:"tags"`
  261. }
  262. func (r *Registry) listGCRImages(repoName string, repo repository.Repository) ([]*Image, error) {
  263. gcp, err := repo.GCPIntegration.ReadGCPIntegration(
  264. r.GCPIntegrationID,
  265. )
  266. if err != nil {
  267. return nil, err
  268. }
  269. // use JWT token to request catalog
  270. client := &http.Client{}
  271. parsedURL, err := url.Parse("https://" + r.URL)
  272. if err != nil {
  273. return nil, err
  274. }
  275. trimmedPath := strings.Trim(parsedURL.Path, "/")
  276. req, err := http.NewRequest(
  277. "GET",
  278. fmt.Sprintf("https://%s/v2/%s/%s/tags/list", parsedURL.Host, trimmedPath, repoName),
  279. nil,
  280. )
  281. if err != nil {
  282. return nil, err
  283. }
  284. req.SetBasicAuth("_json_key", string(gcp.GCPKeyData))
  285. resp, err := client.Do(req)
  286. if err != nil {
  287. return nil, err
  288. }
  289. gcrResp := gcrImageResp{}
  290. if err := json.NewDecoder(resp.Body).Decode(&gcrResp); err != nil {
  291. return nil, fmt.Errorf("Could not read GCR repositories: %v", err)
  292. }
  293. res := make([]*Image, 0)
  294. for _, tag := range gcrResp.Tags {
  295. res = append(res, &Image{
  296. RepositoryName: repoName,
  297. Tag: tag,
  298. })
  299. }
  300. return res, nil
  301. }
  302. func (r *Registry) listDOCRImages(
  303. repoName string,
  304. repo repository.Repository,
  305. doAuth *oauth2.Config,
  306. ) ([]*Image, error) {
  307. oauthInt, err := repo.OAuthIntegration.ReadOAuthIntegration(
  308. r.DOIntegrationID,
  309. )
  310. if err != nil {
  311. return nil, err
  312. }
  313. tok, _, err := oauth.GetAccessToken(oauthInt, doAuth, repo)
  314. if err != nil {
  315. return nil, err
  316. }
  317. client := godo.NewFromToken(tok)
  318. urlArr := strings.Split(r.URL, "/")
  319. if len(urlArr) != 2 {
  320. return nil, fmt.Errorf("invalid digital ocean registry url")
  321. }
  322. name := urlArr[1]
  323. tags, _, err := client.Registry.ListRepositoryTags(context.TODO(), name, repoName, &godo.ListOptions{})
  324. if err != nil {
  325. return nil, err
  326. }
  327. res := make([]*Image, 0)
  328. for _, tag := range tags {
  329. res = append(res, &Image{
  330. RepositoryName: repoName,
  331. Tag: tag.Tag,
  332. })
  333. }
  334. return res, nil
  335. }
  336. // GetDockerConfigJSON returns a dockerconfigjson file contents with "auths"
  337. // populated.
  338. func (r *Registry) GetDockerConfigJSON(
  339. repo repository.Repository,
  340. doAuth *oauth2.Config, // only required if using DOCR
  341. ) ([]byte, error) {
  342. var conf *configfile.ConfigFile
  343. var err error
  344. // switch on the auth mechanism to get a token
  345. if r.AWSIntegrationID != 0 {
  346. conf, err = r.getECRDockerConfigFile(repo)
  347. }
  348. if r.GCPIntegrationID != 0 {
  349. conf, err = r.getGCRDockerConfigFile(repo)
  350. }
  351. if r.DOIntegrationID != 0 {
  352. conf, err = r.getDOCRDockerConfigFile(repo, doAuth)
  353. }
  354. if err != nil {
  355. return nil, err
  356. }
  357. return json.Marshal(conf)
  358. }
  359. func (r *Registry) getECRDockerConfigFile(
  360. repo repository.Repository,
  361. ) (*configfile.ConfigFile, error) {
  362. aws, err := repo.AWSIntegration.ReadAWSIntegration(
  363. r.AWSIntegrationID,
  364. )
  365. if err != nil {
  366. return nil, err
  367. }
  368. sess, err := aws.GetSession()
  369. if err != nil {
  370. return nil, err
  371. }
  372. ecrSvc := ecr.New(sess)
  373. output, err := ecrSvc.GetAuthorizationToken(&ecr.GetAuthorizationTokenInput{})
  374. if err != nil {
  375. return nil, err
  376. }
  377. token := *output.AuthorizationData[0].AuthorizationToken
  378. decodedToken, err := base64.StdEncoding.DecodeString(token)
  379. if err != nil {
  380. return nil, err
  381. }
  382. parts := strings.SplitN(string(decodedToken), ":", 2)
  383. if len(parts) < 2 {
  384. return nil, err
  385. }
  386. key := r.URL
  387. if !strings.Contains(key, "http") {
  388. key = "https://" + key
  389. }
  390. return &configfile.ConfigFile{
  391. AuthConfigs: map[string]types.AuthConfig{
  392. key: types.AuthConfig{
  393. Username: parts[0],
  394. Password: parts[1],
  395. Auth: token,
  396. },
  397. },
  398. }, nil
  399. }
  400. func (r *Registry) getGCRDockerConfigFile(
  401. repo repository.Repository,
  402. ) (*configfile.ConfigFile, error) {
  403. gcp, err := repo.GCPIntegration.ReadGCPIntegration(
  404. r.GCPIntegrationID,
  405. )
  406. if err != nil {
  407. return nil, err
  408. }
  409. key := r.URL
  410. if !strings.Contains(key, "http") {
  411. key = "https://" + key
  412. }
  413. parsedURL, _ := url.Parse(key)
  414. return &configfile.ConfigFile{
  415. AuthConfigs: map[string]types.AuthConfig{
  416. parsedURL.Host: types.AuthConfig{
  417. Username: "_json_key",
  418. Password: string(gcp.GCPKeyData),
  419. Auth: generateAuthToken("_json_key", string(gcp.GCPKeyData)),
  420. },
  421. },
  422. }, nil
  423. }
  424. func (r *Registry) getDOCRDockerConfigFile(
  425. repo repository.Repository,
  426. doAuth *oauth2.Config,
  427. ) (*configfile.ConfigFile, error) {
  428. oauthInt, err := repo.OAuthIntegration.ReadOAuthIntegration(
  429. r.DOIntegrationID,
  430. )
  431. if err != nil {
  432. return nil, err
  433. }
  434. tok, _, err := oauth.GetAccessToken(oauthInt, doAuth, repo)
  435. if err != nil {
  436. return nil, err
  437. }
  438. key := r.URL
  439. if !strings.Contains(key, "http") {
  440. key = "https://" + key
  441. }
  442. parsedURL, _ := url.Parse(key)
  443. return &configfile.ConfigFile{
  444. AuthConfigs: map[string]types.AuthConfig{
  445. parsedURL.Host: types.AuthConfig{
  446. Username: tok,
  447. Password: tok,
  448. Auth: generateAuthToken(tok, tok),
  449. },
  450. },
  451. }, nil
  452. }
  453. func generateAuthToken(username, password string) string {
  454. return base64.StdEncoding.EncodeToString([]byte(username + ":" + password))
  455. }