github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/apiserver/facades/client/resources/facade.go (about)

     1  // Copyright 2017 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package resources
     5  
     6  import (
     7  	"github.com/juju/charm/v12"
     8  	charmresource "github.com/juju/charm/v12/resource"
     9  	"github.com/juju/errors"
    10  	"github.com/juju/loggo"
    11  	"github.com/juju/names/v5"
    12  
    13  	apiresources "github.com/juju/juju/api/client/resources"
    14  	apiservererrors "github.com/juju/juju/apiserver/errors"
    15  	"github.com/juju/juju/apiserver/facade"
    16  	"github.com/juju/juju/apiserver/facades/client/charms"
    17  	"github.com/juju/juju/charmhub"
    18  	corecharm "github.com/juju/juju/core/charm"
    19  	"github.com/juju/juju/core/charm/repository"
    20  	corelogger "github.com/juju/juju/core/logger"
    21  	"github.com/juju/juju/core/resources"
    22  	"github.com/juju/juju/rpc/params"
    23  )
    24  
    25  var logger = loggo.GetLogger("juju.apiserver.resources")
    26  
    27  // Backend is the functionality of Juju's state needed for the resources API.
    28  type Backend interface {
    29  	// ListResources returns the resources for the given application.
    30  	ListResources(application string) (resources.ApplicationResources, error)
    31  
    32  	// AddPendingResource adds the resource to the data backend in a
    33  	// "pending" state. It will stay pending (and unavailable) until
    34  	// it is resolved. The returned ID is used to identify the pending
    35  	// resources when resolving it.
    36  	AddPendingResource(applicationID, userID string, chRes charmresource.Resource) (string, error)
    37  }
    38  
    39  // API is the public API facade for resources.
    40  type API struct {
    41  	// backend is the data source for the facade.
    42  	backend Backend
    43  
    44  	factory func(*charm.URL) (NewCharmRepository, error)
    45  }
    46  
    47  // NewFacade creates a public API facade for resources. It is
    48  // used for API registration.
    49  func NewFacade(ctx facade.Context) (*API, error) {
    50  	authorizer := ctx.Auth()
    51  	if !authorizer.AuthClient() {
    52  		return nil, apiservererrors.ErrPerm
    53  	}
    54  
    55  	st := ctx.State()
    56  	rst := st.Resources()
    57  
    58  	m, err := st.Model()
    59  	if err != nil {
    60  		return nil, errors.Trace(err)
    61  	}
    62  	modelCfg, err := m.Config()
    63  	if err != nil {
    64  		return nil, errors.Trace(err)
    65  	}
    66  
    67  	factory := func(curl *charm.URL) (NewCharmRepository, error) {
    68  		schema := curl.Schema
    69  		switch {
    70  		case charm.CharmHub.Matches(schema):
    71  			chURL, _ := modelCfg.CharmHubURL()
    72  			chClient, err := charmhub.NewClient(charmhub.Config{
    73  				URL:        chURL,
    74  				HTTPClient: ctx.HTTPClient(facade.CharmhubHTTPClient),
    75  				Logger:     logger,
    76  			})
    77  			if err != nil {
    78  				return nil, errors.Trace(err)
    79  			}
    80  			return repository.NewCharmHubRepository(logger.ChildWithLabels("charmhub", corelogger.CHARMHUB), chClient), nil
    81  
    82  		case charm.Local.Matches(schema):
    83  			return &localClient{}, nil
    84  
    85  		default:
    86  			return nil, errors.Errorf("unrecognized charm schema %q", curl.Schema)
    87  		}
    88  	}
    89  
    90  	f, err := NewResourcesAPI(rst, factory)
    91  	if err != nil {
    92  		return nil, errors.Trace(err)
    93  	}
    94  	return f, nil
    95  }
    96  
    97  // NewResourcesAPI returns a new resources API facade.
    98  func NewResourcesAPI(backend Backend, factory func(*charm.URL) (NewCharmRepository, error)) (*API, error) {
    99  	if backend == nil {
   100  		return nil, errors.Errorf("missing data backend")
   101  	}
   102  	if factory == nil {
   103  		// Technically this only matters for one code path through
   104  		// AddPendingResources(). However, that functionality should be
   105  		// provided. So we indicate the problem here instead of later
   106  		// in the specific place where it actually matters.
   107  		return nil, errors.Errorf("missing factory for new repository")
   108  	}
   109  
   110  	f := &API{
   111  		backend: backend,
   112  		factory: factory,
   113  	}
   114  	return f, nil
   115  }
   116  
   117  // ListResources returns the list of resources for the given application.
   118  func (a *API) ListResources(args params.ListResourcesArgs) (params.ResourcesResults, error) {
   119  	var r params.ResourcesResults
   120  	r.Results = make([]params.ResourcesResult, len(args.Entities))
   121  
   122  	for i, e := range args.Entities {
   123  		logger.Tracef("Listing resources for %q", e.Tag)
   124  		tag, apierr := parseApplicationTag(e.Tag)
   125  		if apierr != nil {
   126  			r.Results[i] = params.ResourcesResult{
   127  				ErrorResult: params.ErrorResult{
   128  					Error: apierr,
   129  				},
   130  			}
   131  			continue
   132  		}
   133  
   134  		svcRes, err := a.backend.ListResources(tag.Id())
   135  		if err != nil {
   136  			r.Results[i] = errorResult(err)
   137  			continue
   138  		}
   139  
   140  		r.Results[i] = apiresources.ApplicationResources2APIResult(svcRes)
   141  	}
   142  	return r, nil
   143  }
   144  
   145  // AddPendingResources adds the provided resources (info) to the Juju
   146  // model in a pending state, meaning they are not available until
   147  // resolved. Handles CharmHub and Local charms.
   148  func (a *API) AddPendingResources(args params.AddPendingResourcesArgsV2) (params.AddPendingResourcesResult, error) {
   149  	var result params.AddPendingResourcesResult
   150  
   151  	tag, apiErr := parseApplicationTag(args.Tag)
   152  	if apiErr != nil {
   153  		result.Error = apiErr
   154  		return result, nil
   155  	}
   156  	applicationID := tag.Id()
   157  
   158  	requestedOrigin, err := charms.ConvertParamsOrigin(args.CharmOrigin)
   159  	if err != nil {
   160  		result.Error = apiservererrors.ServerError(err)
   161  		return result, nil
   162  	}
   163  	ids, err := a.addPendingResources(applicationID, args.URL, requestedOrigin, args.Resources)
   164  	if err != nil {
   165  		result.Error = apiservererrors.ServerError(err)
   166  		return result, nil
   167  	}
   168  	result.PendingIDs = ids
   169  	return result, nil
   170  }
   171  
   172  func (a *API) addPendingResources(appName, chRef string, origin corecharm.Origin, apiResources []params.CharmResource) ([]string, error) {
   173  	var resources []charmresource.Resource
   174  	for _, apiRes := range apiResources {
   175  		res, err := apiresources.API2CharmResource(apiRes)
   176  		if err != nil {
   177  			return nil, errors.Annotatef(err, "bad resource info for %q", apiRes.Name)
   178  		}
   179  		resources = append(resources, res)
   180  	}
   181  
   182  	if chRef != "" {
   183  		cURL, err := charm.ParseURL(chRef)
   184  		if err != nil {
   185  			return nil, errors.Trace(err)
   186  		}
   187  		id := corecharm.CharmID{
   188  			URL:    cURL,
   189  			Origin: origin,
   190  		}
   191  		repository, err := a.factory(id.URL)
   192  		if err != nil {
   193  			return nil, errors.Trace(err)
   194  		}
   195  		resources, err = repository.ResolveResources(resources, id)
   196  		if err != nil {
   197  			return nil, errors.Trace(err)
   198  		}
   199  	}
   200  
   201  	var ids []string
   202  	for _, res := range resources {
   203  		pendingID, err := a.addPendingResource(appName, res)
   204  		if err != nil {
   205  			// We don't bother aggregating errors since a partial
   206  			// completion is disruptive and a retry of this endpoint
   207  			// is not expensive.
   208  			return nil, err
   209  		}
   210  		ids = append(ids, pendingID)
   211  	}
   212  	return ids, nil
   213  }
   214  
   215  func (a *API) addPendingResource(appName string, chRes charmresource.Resource) (pendingID string, err error) {
   216  	userID := ""
   217  	pendingID, err = a.backend.AddPendingResource(appName, userID, chRes)
   218  	if err != nil {
   219  		return "", errors.Annotatef(err, "while adding pending resource info for %q", chRes.Name)
   220  	}
   221  	return pendingID, nil
   222  }
   223  
   224  func parseApplicationTag(tagStr string) (names.ApplicationTag, *params.Error) { // note the concrete error type
   225  	ApplicationTag, err := names.ParseApplicationTag(tagStr)
   226  	if err != nil {
   227  		return ApplicationTag, &params.Error{
   228  			Message: err.Error(),
   229  			Code:    params.CodeBadRequest,
   230  		}
   231  	}
   232  	return ApplicationTag, nil
   233  }
   234  
   235  func errorResult(err error) params.ResourcesResult {
   236  	return params.ResourcesResult{
   237  		ErrorResult: params.ErrorResult{
   238  			Error: apiservererrors.ServerError(err),
   239  		},
   240  	}
   241  }