Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

store commit tar in pulp #2706

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,7 @@ func LogConfigAtStartup(cfg *EdgeConfig) {
"RepoFileUploadAttempts": cfg.RepoFileUploadAttempts,
"RepoFileUploadDelay": cfg.RepoFileUploadDelay,
"UploadWorkers": cfg.UploadWorkers,
"PulpURL": cfg.PulpURL,
}

// loop through the key/value pairs
Expand Down
23 changes: 22 additions & 1 deletion pkg/clients/imagebuilder/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -439,12 +439,25 @@ func (c *Client) GetComposeStatus(jobID string) (*ComposeStatus, error) {
"statusCode": res.StatusCode,
"responseBody": string(body),
"error": err,
}).Debug("Image Builder ComposeStatus Response")
}).Trace("Image Builder ComposeStatus Response")

if err != nil {
c.log.WithFields(log.Fields{
"statusCode": res.StatusCode,
"responseBody": string(body),
"error": err,
}).Error("Error reading compose status response")

return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
c.log.WithFields(log.Fields{
"statusCode": res.StatusCode,
"responseBody": string(body),
"error": err,
}).Error("Error compose status HTTP response not StatusOK")

return nil, fmt.Errorf("request for status was not successful")
}

Expand All @@ -456,6 +469,14 @@ func (c *Client) GetComposeStatus(jobID string) (*ComposeStatus, error) {
return nil, err
}

if cs.ImageStatus.Status == imageStatusSuccess {
c.log.WithFields(log.Fields{
"statusCode": res.StatusCode,
"responseBody": string(body),
"error": err,
}).Info("Image Builder ComposeStatus Response")
}

return cs, nil
}

Expand Down
8 changes: 7 additions & 1 deletion pkg/routes/images.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,13 @@ func CreateImage(w http.ResponseWriter, r *http.Request) {
return
}

ctxServices.Log.Debug("Creating image from API request")
storageBackend := "AWS"
if feature.PulpIntegration.IsEnabled() {
storageBackend = "Pulp"
}

ctxServices.Log.WithField("storage_backend", storageBackend).Debug("Creating image from API request")

// initial checks and filling in necessary image info
if err = ctxServices.ImageService.CreateImage(image); err != nil {
ctxServices.Log.WithField("error", err.Error()).Error("Failed creating the image")
Expand Down
5 changes: 4 additions & 1 deletion pkg/services/images.go
Original file line number Diff line number Diff line change
Expand Up @@ -878,11 +878,14 @@ func (s *ImageService) CreateRepoForImage(ctx context.Context, img *models.Image
}
s.log.Debug("OSTree repo was saved to commit")

// TODO: move this to repobuilder and replace with single call to StoreRepo w/ interface
var repository *models.Repo
var err error
if feature.PulpIntegration.IsEnabled() {
repository, err = s.RepoBuilder.StoreRepo(repo)
s.log.Debug("Running Pulp repo process")
repository, err = s.RepoBuilder.StoreRepo(ctx, repo)
} else {
s.log.Debug("Running AWS repo process")
repository, err = s.RepoBuilder.ImportRepo(repo)
}
if err != nil {
Expand Down
270 changes: 270 additions & 0 deletions pkg/services/internal/pulpcommit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
package repostore
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The package does not match: internal vs repostore

I suggest this code is not tightly coupled to model, meaning it will only accept primitive types if that is possible. It can live either directly in clients/pulp, or you can create a new separate package for it.


import (
"context"
"errors"
"fmt"
"net/url"
"strings"

"github.com/google/uuid"
"github.com/redhatinsights/edge-api/config"
"github.com/redhatinsights/edge-api/pkg/clients/pulp"
"github.com/redhatinsights/edge-api/pkg/models"

log "github.com/sirupsen/logrus"
)

// PulpCommit relates to the storage of a commit tarfile in Pulp
type PulpCommit struct {
OrgID string
SourceURL string
Domain *pulpDomain
FileRepo *pulpFileRepository
OSTreeRepo *pulpOSTreeRepository
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Type used as a "class", not needed.


// Store imports an OSTree repo into Pulp
func (pc *PulpCommit) Store(ctx context.Context, commit models.Commit, edgeRepo *models.Repo) error {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Parameter named commit does not need to be there, only OrgID is used. The same for edgeRepo, try to minimize dependencies of this package, just use primitive types.


// create a domain with a pre-defined name
pc.Domain = &pulpDomain{
Name: fmt.Sprintf("em%sd", commit.OrgID),
}
derr := pc.Domain.Create(ctx)
if derr != nil {
return derr
}

// get a pulp service based on the specific domain
pserv, err := pulp.NewPulpServiceWithDomain(ctx, pc.Domain.Name)
if err != nil {
return err
}

// Import the commit tarfile into an initial Pulp file repo
pc.FileRepo = &pulpFileRepository{}
if err = pc.FileRepo.Import(ctx, pserv, pc.SourceURL); err != nil {
log.WithContext(ctx).Error("Error importing tarfile to initial pulp file repository")
return err
}
log.WithContext(ctx).WithFields(log.Fields{
"artifact": pc.FileRepo.artifact,
"version": pc.FileRepo.version,
}).Info("Pulp artifact uploaded")

// create an OSTree repository in Pulp
// TODO: check for an existing OSTree repo for image commit versions > 1 and add the commit
pc.OSTreeRepo = &pulpOSTreeRepository{
Name: fmt.Sprintf("repo-%s-%d", commit.OrgID, edgeRepo.ID),
}
if err = pc.OSTreeRepo.Create(ctx, pserv, pc.OrgID); err != nil {
log.WithContext(ctx).Error("Error creating pulp ostree repository")
return err
}
log.WithContext(ctx).Info("Pulp OSTree Repo created with Content Guard and Distribution")

if err = pc.OSTreeRepo.Import(ctx, pserv, pc.OSTreeRepo.Name, edgeRepo, *pc.FileRepo); err != nil {
log.WithContext(ctx).Error("Error importing tarfile into pulp ostree repository")

return err
}
log.WithContext(ctx).Info("Image Builder commit tarfile imported into pulp ostree repo from pulp file repo")

return nil
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The types and interfaces you introduced makes this function worse in my opinion. Readability is king, if I have to choose between this and a single function that can be read top to bottom, I am going for the long function even if I am breaking up the dogma of "function must not be longer than N lines". Functions can always be easily broken down to sub-functions and arguments can be grouped together into structs that are only used as data carriers.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm passing the service in with the interface for unittesting.
I'm more of a fan of functional and small for those tests. Agree the methods/receiver can be overkill.

}

type pulpDomain struct {
Name string
UUID uuid.UUID
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see little benefit in this type, domain can be just string, in fact that is exactly what the pulp client wants.


// Create creates a domain for a specific org if one does not already exist
func (pd *pulpDomain) Create(ctx context.Context) error {
pulpDefaultService, err := pulp.NewPulpServiceDefaultDomain(ctx)
if err != nil {
return err
}

domains, err := pulpDefaultService.DomainsList(ctx, pd.Name)
if err != nil {
log.WithContext(ctx).WithFields(log.Fields{
"domain_name": pd.Name,
"error": err.Error(),
}).Error("Error listing pulp domains")
return err
}

if len(domains) > 1 {
log.WithContext(ctx).WithFields(log.Fields{
"name": pd.Name,
"domains": domains,
}).Error("More than one domain matches name")
return errors.New("More than one domain matches name")
}

if len(domains) == 0 {
createdDomain, err := pulpDefaultService.DomainsCreate(ctx, pd.Name)
if err != nil {
log.WithContext(ctx).WithField("error", err.Error()).Error("Error creating pulp domain")
return err
}

pd.UUID = pulp.ScanUUID(createdDomain.PulpHref)

log.WithContext(ctx).WithFields(log.Fields{
"domain_name": createdDomain.Name,
"domain_href": createdDomain.PulpHref,
"domain_uuid": pd.UUID,
}).Info("Created new pulp domain")
}

return nil
}

type pulpFileRepository struct {
repo string
artifact string
version string
}

type pulpFileRepoImporter interface {
FileRepositoriesEnsure(context.Context) (string, error)
FileRepositoriesImport(context.Context, string, string) (string, string, error)
}

func (pfr *pulpFileRepository) Import(ctx context.Context, pulpService pulpFileRepoImporter, sourceURL string) error {

// get the file repo to initially push the tar artifact
var err error
pfr.repo, err = pulpService.FileRepositoriesEnsure(ctx)
if err != nil {
return err
}

log.WithContext(ctx).Info("File repo found or created: ", pfr.repo)

pfr.artifact, pfr.version, err = pulpService.FileRepositoriesImport(ctx, pfr.repo, sourceURL)
if err != nil {
return err
}

return nil
}

type pulpOSTreeRepository struct {
ID uint
Name string
CGPulpHref string
PulpHref string
Distribution *pulp.OstreeOstreeDistributionResponse
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto.


type pulpOSTreeRepositoryCreator interface {
RepositoriesCreate(context.Context, string) (*pulp.OstreeOstreeRepositoryResponse, error)
ContentGuardEnsure(context.Context, string) (*pulp.CompositeContentGuardResponse, error)
DistributionsCreate(context.Context, string, string, string, string) (*pulp.OstreeOstreeDistributionResponse, error)
}

// Create creates a new Pulp OSTree repository for a commit
func (pr *pulpOSTreeRepository) Create(ctx context.Context, pulpService pulpOSTreeRepositoryCreator, orgID string) error {

pulpRepo, err := pulpService.RepositoriesCreate(ctx, pr.Name)
if err != nil {
return err
}
pr.PulpHref = *pulpRepo.PulpHref
log.WithContext(ctx).WithField("pulp_href", pr.PulpHref).Info("Pulp Repository created")

cg, err := pulpService.ContentGuardEnsure(ctx, orgID)
if err != nil {
return err
}
pr.CGPulpHref = *cg.PulpHref
log.WithContext(ctx).WithFields(log.Fields{
"contentguard_href": pr.CGPulpHref,
"contentguard_0": (*cg.Guards)[0],
"contentguard_1": (*cg.Guards)[1],
}).Info("Pulp Content Guard found or created")

pr.Distribution, err = pulpService.DistributionsCreate(ctx, pr.Name, pr.Name, *pulpRepo.PulpHref, *cg.PulpHref)
if err != nil {
return err
}
log.WithContext(ctx).WithFields(log.Fields{
"name": pr.Distribution.Name,
"base_path": pr.Distribution.BasePath,
"base_url": pr.Distribution.BaseUrl,
"pulp_href": pr.Distribution.PulpHref,
}).Info("Pulp Distribution created")

return nil
}

// DistributionURL returns the password embedded distribution URL for a specific repo
func (pr *pulpOSTreeRepository) DistributionURL(ctx context.Context, domain string, repoName string) (string, error) {
cfg := config.Get()

distBaseURL := *pr.Distribution.BaseUrl
prodDistURL, err := url.Parse(distBaseURL)
if err != nil {
return "", errors.New("Unable to set user:password for Pulp distribution URL")
}
prodDistURL.User = url.UserPassword(cfg.PulpContentUsername, cfg.PulpContentPassword)
distURL := prodDistURL.String()

// temporarily handle stage URLs so Image Builder worker can get to stage Pulp
if strings.Contains(distBaseURL, "stage") {
stagePulpURL := fmt.Sprintf("%s/api/pulp-content/%s/%s", cfg.PulpURL, domain, repoName)
stageDistURL, err := url.Parse(stagePulpURL)
if err != nil {
return "", errors.New("Unable to set user:password for Pulp distribution URL")
}
stageDistURL.User = url.UserPassword(cfg.PulpContentUsername, cfg.PulpContentPassword)

distURL = stageDistURL.String()
}

parsedDistURL, _ := url.Parse(distURL)
log.WithContext(ctx).WithField("distribution_url", parsedDistURL.Redacted()).Debug("Distribution URL retrieved")

return distURL, err
}

type pulpOSTreeRepositoryImporter interface {
RepositoriesImport(context.Context, uuid.UUID, string, string) (*pulp.OstreeOstreeRepositoryResponse, error)
FileRepositoriesVersionDelete(context.Context, uuid.UUID, int64) error
Domain() string
}

// Import imports an artifact into the repo and deletes the tarfile artifact
func (pr *pulpOSTreeRepository) Import(ctx context.Context, pulpService pulpOSTreeRepositoryImporter, pulpRepoName string, edgeRepo *models.Repo, fileRepo pulpFileRepository) error {
log.WithContext(ctx).Debug("Starting tarfile import into Pulp OSTree repository")
repoImported, err := pulpService.RepositoriesImport(ctx, pulp.ScanUUID(&pr.PulpHref), "repo", fileRepo.artifact)
if err != nil {
return err
}
log.WithContext(ctx).Info("Repository imported", *repoImported.PulpHref)

edgeRepo.URL, err = pr.DistributionURL(ctx, pulpService.Domain(), pulpRepoName)
if err != nil {
log.WithContext(ctx).WithField("error", err.Error()).Error("Error getting distibution URL for Pulp repo")
}

// TODO: move this to parallel or end of entire process (post Installer if requested)
err = pulpService.FileRepositoriesVersionDelete(ctx, pulp.ScanUUID(&fileRepo.version), pulp.ScanRepoFileVersion(&fileRepo.version))
if err != nil {
return err
}
log.WithContext(ctx).Info("Artifact version deleted", fileRepo.version)

edgeRepo.Status = models.RepoStatusSuccess

edgeRepoURL, _ := url.Parse(edgeRepo.URL)
log.WithContext(ctx).WithFields(log.Fields{
"status": edgeRepo.Status,
"repo_distribution_url": edgeRepoURL.Redacted(),
}).Debug("Repo import into Pulp complete")

return nil
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could have been just a single function, granted very long but it could be broken down to:

func DoSomething(a, b, c, d) (result, finish, error) {
 doStep1(a, b, c)
 doStep2(b, c, d)
 return result, func(){ deleteRepository(b, c) }, nil
}

func doStep1(a, b, c)

func doStep2(b, c, d)

It returns a result (status) that can be stored into database, a function that can be deferred and performs a deletion of resources that are no longer needed and of course an error in case things go wrong. The caller:

r, finish, err := DoSomething(a, b, c, d)
if err != nil {
 ...
}
defer finish()
repository.Status = r
repository.Save()

A small step above would be introducing struct types (without any methods) just to group some arguments together in case the sub-functions have too many arguments. And only if this does not work, I would consider creating a new object type:

type Importer struct {
 Input1 type,
 Input2 type,
 Input3 type,
 private1 type,
 private2 type,
 private3 type,
}

func (i *Importer) DoSomething() (r, finish, err)

9 changes: 5 additions & 4 deletions pkg/services/mock_services/repobuilder.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading