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

     1  // Copyright 2021 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package application
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  
    10  	"github.com/juju/errors"
    11  	"github.com/juju/names/v5"
    12  
    13  	apiservererrors "github.com/juju/juju/apiserver/errors"
    14  	"github.com/juju/juju/charmhub"
    15  	"github.com/juju/juju/charmhub/transport"
    16  	corebase "github.com/juju/juju/core/base"
    17  	corecharm "github.com/juju/juju/core/charm"
    18  	"github.com/juju/juju/rpc/params"
    19  	"github.com/juju/juju/state"
    20  )
    21  
    22  // CharmhubClient represents a way for querying the charmhub api for information
    23  // about the application charm.
    24  type CharmhubClient interface {
    25  	Refresh(ctx context.Context, config charmhub.RefreshConfig) ([]transport.RefreshResponse, error)
    26  }
    27  
    28  // UpdateBase defines an interface for interacting with updating a base.
    29  type UpdateBase interface {
    30  
    31  	// UpdateBase attempts to update an application base for deploying new
    32  	// units.
    33  	UpdateBase(string, corebase.Base, bool) error
    34  }
    35  
    36  // UpdateBaseState defines a common set of functions for retrieving state
    37  // objects.
    38  type UpdateBaseState interface {
    39  	// Application returns a list of all the applications for a
    40  	// given machine. This includes all the subordinates.
    41  	Application(string) (Application, error)
    42  }
    43  
    44  // UpdateBaseValidator defines an application validator.
    45  type UpdateBaseValidator interface {
    46  	// ValidateApplication attempts to validate an application for
    47  	// a given base. Using force to allow the overriding of the error to
    48  	// ensure all application validate.
    49  	//
    50  	// I do question if you actually need to validate anything if force is
    51  	// employed here?
    52  	ValidateApplication(application Application, base corebase.Base, force bool) error
    53  }
    54  
    55  // UpdateBaseAPI provides the update series API facade for any given version.
    56  // It is expected that any API parameter changes should be performed before
    57  // entering the API.
    58  type UpdateBaseAPI struct {
    59  	state     UpdateBaseState
    60  	validator UpdateBaseValidator
    61  }
    62  
    63  // NewUpdateBaseAPI creates a new UpdateBaseAPI
    64  func NewUpdateBaseAPI(
    65  	state UpdateBaseState,
    66  	validator UpdateBaseValidator,
    67  ) *UpdateBaseAPI {
    68  	return &UpdateBaseAPI{
    69  		state:     state,
    70  		validator: validator,
    71  	}
    72  }
    73  
    74  func (a *UpdateBaseAPI) UpdateBase(tag string, base corebase.Base, force bool) error {
    75  	if base.String() == "" {
    76  		return errors.BadRequestf("base missing from args")
    77  	}
    78  	applicationTag, err := names.ParseApplicationTag(tag)
    79  	if err != nil {
    80  		return errors.Trace(err)
    81  	}
    82  	app, err := a.state.Application(applicationTag.Id())
    83  	if err != nil {
    84  		return errors.Trace(err)
    85  	}
    86  	if !app.IsPrincipal() {
    87  		return &params.Error{
    88  			Message: fmt.Sprintf("%q is a subordinate application, update-series not supported", applicationTag.Id()),
    89  			Code:    params.CodeNotSupported,
    90  		}
    91  	}
    92  
    93  	if err := a.validator.ValidateApplication(app, base, force); err != nil {
    94  		return errors.Trace(err)
    95  	}
    96  
    97  	return app.UpdateApplicationBase(state.Base{
    98  		OS: base.OS, Channel: base.Channel.String(),
    99  	}, force)
   100  }
   101  
   102  type updateSeriesValidator struct {
   103  	localValidator  UpdateBaseValidator
   104  	remoteValidator UpdateBaseValidator
   105  }
   106  
   107  func makeUpdateSeriesValidator(client CharmhubClient) updateSeriesValidator {
   108  	return updateSeriesValidator{
   109  		localValidator: stateSeriesValidator{},
   110  		remoteValidator: charmhubSeriesValidator{
   111  			client: client,
   112  		},
   113  	}
   114  }
   115  
   116  func (s updateSeriesValidator) ValidateApplication(app Application, base corebase.Base, force bool) error {
   117  	// This is not a charmhub charm, so we can fallback to querying state
   118  	// for the supported series.
   119  	if origin := app.CharmOrigin(); origin == nil || !corecharm.CharmHub.Matches(origin.Source) {
   120  		return s.localValidator.ValidateApplication(app, base, force)
   121  	}
   122  
   123  	return s.remoteValidator.ValidateApplication(app, base, force)
   124  }
   125  
   126  // stateSeriesValidator validates an application using the state (database)
   127  // version of the charm.
   128  // NOTE: stateSeriesValidator also exists in apiserver/facades/client/machinemanager/upgrade_series.go,
   129  // When making changes here, review the copy for required changes as well.
   130  type stateSeriesValidator struct{}
   131  
   132  // ValidateApplication attempts to validate an applications for
   133  // a given base.
   134  func (s stateSeriesValidator) ValidateApplication(application Application, base corebase.Base, force bool) error {
   135  	ch, _, err := application.Charm()
   136  	if err != nil {
   137  		return errors.Trace(err)
   138  	}
   139  	supportedBases, err := corecharm.ComputedBases(ch)
   140  	if err != nil {
   141  		return errors.Trace(err)
   142  	}
   143  	if len(supportedBases) == 0 {
   144  		err := errors.NewNotSupported(nil, fmt.Sprintf("charm %q does not support any bases. Not valid", ch.Meta().Name))
   145  		return apiservererrors.ServerError(err)
   146  	}
   147  	_, baseSupportedErr := corecharm.BaseForCharm(base, supportedBases)
   148  	if baseSupportedErr != nil && !force {
   149  		return apiservererrors.NewErrIncompatibleBase(supportedBases, base, ch.Meta().Name)
   150  	}
   151  	return nil
   152  }
   153  
   154  // NOTE: charmhubSeriesValidator also exists in apiserver/facades/client/machinemanager/upgrade_series.go,
   155  // When making changes here, review the copy for required changes as well.
   156  type charmhubSeriesValidator struct {
   157  	client CharmhubClient
   158  }
   159  
   160  // ValidateApplication attempts to validate an application for
   161  // a given base.
   162  func (s charmhubSeriesValidator) ValidateApplication(application Application, base corebase.Base, force bool) error {
   163  	// We can be assured that the charm origin is not nil, because we
   164  	// guarded against it before.
   165  	origin := application.CharmOrigin()
   166  	rev := origin.Revision
   167  	if rev == nil {
   168  		return errors.Errorf("no revision found for application %q", application.Name())
   169  	}
   170  
   171  	cfg, err := charmhub.DownloadOneFromRevision(origin.ID, *rev)
   172  	if err != nil {
   173  		return errors.Trace(err)
   174  	}
   175  
   176  	refreshResp, err := s.client.Refresh(context.TODO(), cfg)
   177  	if err != nil {
   178  		return errors.Trace(err)
   179  	}
   180  	if len(refreshResp) != 1 {
   181  		return errors.Errorf("unexpected number of responses %d for applications 1", len(refreshResp))
   182  	}
   183  	for _, resp := range refreshResp {
   184  		if err := resp.Error; err != nil && !force {
   185  			return errors.Annotatef(err, "unable to locate application with base %s", base.DisplayString())
   186  		}
   187  	}
   188  	// DownloadOneFromRevision does not take a base, however the response contains the bases
   189  	// supported by the given revision.  Validate against provided series.
   190  	channelToValidate := base.Channel.Track
   191  	for _, resp := range refreshResp {
   192  		var found bool
   193  		for _, base := range resp.Entity.Bases {
   194  			if channelToValidate == base.Channel || force {
   195  				found = true
   196  				break
   197  			}
   198  		}
   199  		if !found {
   200  			return errors.Errorf("charm %q does not support %s, force not used", resp.Name, base.DisplayString())
   201  		}
   202  	}
   203  	return nil
   204  }