From e3e7e84821bdf240072c7504434acfd28b31be23 Mon Sep 17 00:00:00 2001 From: Felix Han Date: Tue, 30 Mar 2021 10:58:56 +1300 Subject: [PATCH 1/2] feat(ACI): add UAC to ACI --- .../resourcecontrol_create.go | 2 + api/http/proxy/factory/azure.go | 4 +- .../proxy/factory/azure/access_control.go | 149 ++++++++++++++++++ .../proxy/factory/azure/containergroup.go | 109 +++++++++++++ .../proxy/factory/azure/containergroups.go | 48 ++++++ api/http/proxy/factory/azure/transport.go | 28 +++- api/http/proxy/factory/factory.go | 2 +- .../proxy/factory/responseutils/response.go | 19 ++- api/internal/authorization/access_control.go | 6 +- api/portainer.go | 7 + .../containerGroupsDatatable.html | 13 ++ .../containerGroupsDatatable.js | 2 +- app/azure/models/container_group.js | 8 + .../containerInstanceDetails.html | 4 + .../createContainerInstanceController.js | 20 ++- .../create/createcontainerinstance.html | 3 + .../resourceControl/resourceControlTypes.js | 2 + app/portainer/services/notifications.js | 2 + 18 files changed, 410 insertions(+), 18 deletions(-) create mode 100644 api/http/proxy/factory/azure/access_control.go create mode 100644 api/http/proxy/factory/azure/containergroup.go create mode 100644 api/http/proxy/factory/azure/containergroups.go diff --git a/api/http/handler/resourcecontrols/resourcecontrol_create.go b/api/http/handler/resourcecontrols/resourcecontrol_create.go index cf943b678..e57476ed5 100644 --- a/api/http/handler/resourcecontrols/resourcecontrol_create.go +++ b/api/http/handler/resourcecontrols/resourcecontrol_create.go @@ -78,6 +78,8 @@ func (handler *Handler) resourceControlCreate(w http.ResponseWriter, r *http.Req switch payload.Type { case "container": resourceControlType = portainer.ContainerResourceControl + case "container-group": + resourceControlType = portainer.ContainerGroupResourceControl case "service": resourceControlType = portainer.ServiceResourceControl case "volume": diff --git a/api/http/proxy/factory/azure.go b/api/http/proxy/factory/azure.go index 27b8a26f8..84c9c6495 100644 --- a/api/http/proxy/factory/azure.go +++ b/api/http/proxy/factory/azure.go @@ -8,13 +8,13 @@ import ( "github.com/portainer/portainer/api/http/proxy/factory/azure" ) -func newAzureProxy(endpoint *portainer.Endpoint) (http.Handler, error) { +func newAzureProxy(endpoint *portainer.Endpoint, dataStore portainer.DataStore) (http.Handler, error) { remoteURL, err := url.Parse(azureAPIBaseURL) if err != nil { return nil, err } proxy := newSingleHostReverseProxyWithHostHeader(remoteURL) - proxy.Transport = azure.NewTransport(&endpoint.AzureCredentials) + proxy.Transport = azure.NewTransport(&endpoint.AzureCredentials, dataStore, endpoint) return proxy, nil } diff --git a/api/http/proxy/factory/azure/access_control.go b/api/http/proxy/factory/azure/access_control.go new file mode 100644 index 000000000..9974bfca1 --- /dev/null +++ b/api/http/proxy/factory/azure/access_control.go @@ -0,0 +1,149 @@ +package azure + +import ( + "log" + "net/http" + + portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/security" + "github.com/portainer/portainer/api/internal/authorization" +) + +func (transport *Transport) createAzureRequestContext(request *http.Request) (*azureRequestContext, error) { + var err error + + tokenData, err := security.RetrieveTokenData(request) + if err != nil { + return nil, err + } + + resourceControls, err := transport.dataStore.ResourceControl().ResourceControls() + if err != nil { + return nil, err + } + + context := &azureRequestContext{ + isAdmin: true, + userID: tokenData.ID, + resourceControls: resourceControls, + } + + if tokenData.Role != portainer.AdministratorRole { + context.isAdmin = false + + teamMemberships, err := transport.dataStore.TeamMembership().TeamMembershipsByUserID(tokenData.ID) + if err != nil { + return nil, err + } + + userTeamIDs := make([]portainer.TeamID, 0) + for _, membership := range teamMemberships { + userTeamIDs = append(userTeamIDs, membership.TeamID) + } + context.userTeamIDs = userTeamIDs + } + + return context, nil +} + +func decorateObject(object map[string]interface{}, resourceControl *portainer.ResourceControl) map[string]interface{} { + if object["Portainer"] == nil { + object["Portainer"] = make(map[string]interface{}) + } + + portainerMetadata := object["Portainer"].(map[string]interface{}) + portainerMetadata["ResourceControl"] = resourceControl + return object +} + +func (transport *Transport) createPrivateResourceControl( + resourceIdentifier string, + resourceType portainer.ResourceControlType, + userID portainer.UserID) (*portainer.ResourceControl, error) { + + resourceControl := authorization.NewPrivateResourceControl(resourceIdentifier, resourceType, userID) + + err := transport.dataStore.ResourceControl().CreateResourceControl(resourceControl) + if err != nil { + log.Printf("[ERROR] [http,proxy,azure,transport] [message: unable to persist resource control] [resource: %s] [err: %s]", resourceIdentifier, err) + return nil, err + } + + return resourceControl, nil +} + +func (transport *Transport) userCanDeleteContainerGroup(request *http.Request, context *azureRequestContext) bool { + if context.isAdmin { + return true + } + resourceIdentifier := request.URL.Path + resourceControl := transport.findResourceControl(resourceIdentifier, context) + return authorization.UserCanAccessResource(context.userID, context.userTeamIDs, resourceControl) +} + +func (transport *Transport) decorateContainerGroups(containerGroups []interface{}, context *azureRequestContext) []interface{} { + decoratedContainerGroups := make([]interface{}, 0) + + for _, containerGroup := range containerGroups { + containerGroup = transport.decorateContainerGroup(containerGroup.(map[string]interface{}), context) + decoratedContainerGroups = append(decoratedContainerGroups, containerGroup) + } + + return decoratedContainerGroups +} + +func (transport *Transport) decorateContainerGroup(containerGroup map[string]interface{}, context *azureRequestContext) map[string]interface{} { + containerGroupId, ok := containerGroup["id"].(string) + if ok { + resourceControl := transport.findResourceControl(containerGroupId, context) + if resourceControl != nil { + containerGroup = decorateObject(containerGroup, resourceControl) + } + } else { + log.Printf("[WARN] [http,proxy,azure,decorate] [message: unable to find resource id property in container group]") + } + + return containerGroup +} + +func (transport *Transport) filterContainerGroups(containerGroups []interface{}, context *azureRequestContext) []interface{} { + filteredContainerGroups := make([]interface{}, 0) + + for _, containerGroup := range containerGroups { + userCanAccessResource := false + containerGroup := containerGroup.(map[string]interface{}) + portainerObject, ok := containerGroup["Portainer"].(map[string]interface{}) + if ok { + resourceControl, ok := portainerObject["ResourceControl"].(*portainer.ResourceControl) + if ok { + userCanAccessResource = authorization.UserCanAccessResource(context.userID, context.userTeamIDs, resourceControl) + } + } + + if context.isAdmin || userCanAccessResource { + filteredContainerGroups = append(filteredContainerGroups, containerGroup) + } + } + + return filteredContainerGroups +} + +func (transport *Transport) removeResourceControl(containerGroup map[string]interface{}, context *azureRequestContext) error { + containerGroupID, ok := containerGroup["id"].(string) + if ok { + resourceControl := transport.findResourceControl(containerGroupID, context) + if resourceControl != nil { + err := transport.dataStore.ResourceControl().DeleteResourceControl(resourceControl.ID) + return err + } + } else { + log.Printf("[WARN] [http,proxy,azure] [message: missign ID in container group]") + } + + return nil +} + +func (transport *Transport) findResourceControl(containerGroupId string, context *azureRequestContext) *portainer.ResourceControl { + resourceControl := authorization.GetResourceControlByResourceIDAndType(containerGroupId, portainer.ContainerGroupResourceControl, context.resourceControls) + return resourceControl +} diff --git a/api/http/proxy/factory/azure/containergroup.go b/api/http/proxy/factory/azure/containergroup.go new file mode 100644 index 000000000..4fc0042b9 --- /dev/null +++ b/api/http/proxy/factory/azure/containergroup.go @@ -0,0 +1,109 @@ +package azure + +import ( + "errors" + "net/http" + + portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/proxy/factory/responseutils" +) + +// proxy for /subscriptions/*/resourceGroups/*/providers/Microsoft.ContainerInstance/containerGroups/* +func (transport *Transport) proxyContainerGroupRequest(request *http.Request) (*http.Response, error) { + switch request.Method { + case http.MethodPut: + return transport.proxyContainerGroupPutRequest(request) + case http.MethodGet: + return transport.proxyContainerGroupGetRequest(request) + case http.MethodDelete: + return transport.proxyContainerGroupDeleteRequest(request) + default: + return http.DefaultTransport.RoundTrip(request) + } +} + +func (transport *Transport) proxyContainerGroupPutRequest(request *http.Request) (*http.Response, error) { + response, err := http.DefaultTransport.RoundTrip(request) + if err != nil { + return response, err + } + + responseObject, err := responseutils.GetResponseAsJSONOBject(response) + if err != nil { + return response, err + } + + containerGroupID, ok := responseObject["id"].(string) + if !ok { + return response, errors.New("Missing container group ID") + } + + context, err := transport.createAzureRequestContext(request) + if err != nil { + return response, err + } + + resourceControl, err := transport.createPrivateResourceControl(containerGroupID, portainer.ContainerGroupResourceControl, context.userID) + if err != nil { + return response, err + } + + responseObject = decorateObject(responseObject, resourceControl) + + err = responseutils.RewriteResponse(response, responseObject, http.StatusOK) + if err != nil { + return response, err + } + + return response, nil +} + +func (transport *Transport) proxyContainerGroupGetRequest(request *http.Request) (*http.Response, error) { + response, err := http.DefaultTransport.RoundTrip(request) + if err != nil { + return response, err + } + + responseObject, err := responseutils.GetResponseAsJSONOBject(response) + if err != nil { + return nil, err + } + + context, err := transport.createAzureRequestContext(request) + if err != nil { + return nil, err + } + + responseObject = transport.decorateContainerGroup(responseObject, context) + + responseutils.RewriteResponse(response, responseObject, http.StatusOK) + + return response, nil +} + +func (transport *Transport) proxyContainerGroupDeleteRequest(request *http.Request) (*http.Response, error) { + context, err := transport.createAzureRequestContext(request) + if err != nil { + return nil, err + } + + if !transport.userCanDeleteContainerGroup(request, context) { + return responseutils.WriteAccessDeniedResponse() + } + + response, err := http.DefaultTransport.RoundTrip(request) + if err != nil { + return response, err + } + + responseObject, err := responseutils.GetResponseAsJSONOBject(response) + if err != nil { + return nil, err + } + + transport.removeResourceControl(responseObject, context) + + responseutils.RewriteResponse(response, responseObject, http.StatusOK) + + return response, nil +} diff --git a/api/http/proxy/factory/azure/containergroups.go b/api/http/proxy/factory/azure/containergroups.go new file mode 100644 index 000000000..b13d0c924 --- /dev/null +++ b/api/http/proxy/factory/azure/containergroups.go @@ -0,0 +1,48 @@ +package azure + +import ( + "fmt" + "net/http" + + "github.com/portainer/portainer/api/http/proxy/factory/responseutils" +) + +// proxy for /subscriptions/*/providers/Microsoft.ContainerInstance/containerGroups +func (transport *Transport) proxyContainerGroupsRequest(request *http.Request) (*http.Response, error) { + switch request.Method { + case http.MethodGet: + return transport.proxyContainerGroupsGetRequest(request) + default: + return http.DefaultTransport.RoundTrip(request) + } +} + +func (transport *Transport) proxyContainerGroupsGetRequest(request *http.Request) (*http.Response, error) { + response, err := http.DefaultTransport.RoundTrip(request) + if err != nil { + return nil, err + } + + responseObject, err := responseutils.GetResponseAsJSONOBject(response) + if err != nil { + return nil, err + } + + value, ok := responseObject["value"].([]interface{}) + if ok { + context, err := transport.createAzureRequestContext(request) + if err != nil { + return response, err + } + + decoratedValue := transport.decorateContainerGroups(value, context) + filteredValue := transport.filterContainerGroups(decoratedValue, context) + responseObject["value"] = filteredValue + + responseutils.RewriteResponse(response, responseObject, http.StatusOK) + } else { + return nil, fmt.Errorf("The container groups response has no value property") + } + + return response, nil +} \ No newline at end of file diff --git a/api/http/proxy/factory/azure/transport.go b/api/http/proxy/factory/azure/transport.go index 0c8505c8b..d6ed7de7a 100644 --- a/api/http/proxy/factory/azure/transport.go +++ b/api/http/proxy/factory/azure/transport.go @@ -5,7 +5,7 @@ import ( "strconv" "sync" "time" - + "path" "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/http/client" ) @@ -21,26 +21,50 @@ type ( client *client.HTTPClient token *azureAPIToken mutex sync.Mutex + dataStore portainer.DataStore + endpoint *portainer.Endpoint + } + + azureRequestContext struct { + isAdmin bool + userID portainer.UserID + userTeamIDs []portainer.TeamID + resourceControls []portainer.ResourceControl } ) // NewTransport returns a pointer to a new instance of Transport that implements the HTTP Transport // interface for proxying requests to the Azure API. -func NewTransport(credentials *portainer.AzureCredentials) *Transport { +func NewTransport(credentials *portainer.AzureCredentials, dataStore portainer.DataStore, endpoint *portainer.Endpoint) *Transport { return &Transport{ credentials: credentials, client: client.NewHTTPClient(), + dataStore: dataStore, + endpoint: endpoint, } } // RoundTrip is the implementation of the the http.RoundTripper interface func (transport *Transport) RoundTrip(request *http.Request) (*http.Response, error) { + return transport.proxyAzureRequest(request) +} + +func (transport *Transport) proxyAzureRequest(request *http.Request) (*http.Response, error) { + requestPath := request.URL.Path + err := transport.retrieveAuthenticationToken() if err != nil { return nil, err } request.Header.Set("Authorization", "Bearer "+transport.token.value) + + if match, _ := path.Match(portainer.AzurePathContainerGroups, requestPath); match { + return transport.proxyContainerGroupsRequest(request) + } else if match, _ := path.Match(portainer.AzurePathContainerGroup, requestPath); match { + return transport.proxyContainerGroupRequest(request) + } + return http.DefaultTransport.RoundTrip(request) } diff --git a/api/http/proxy/factory/factory.go b/api/http/proxy/factory/factory.go index dd4e62440..1e6f83d40 100644 --- a/api/http/proxy/factory/factory.go +++ b/api/http/proxy/factory/factory.go @@ -55,7 +55,7 @@ func (factory *ProxyFactory) NewLegacyExtensionProxy(extensionAPIURL string) (ht func (factory *ProxyFactory) NewEndpointProxy(endpoint *portainer.Endpoint) (http.Handler, error) { switch endpoint.Type { case portainer.AzureEnvironment: - return newAzureProxy(endpoint) + return newAzureProxy(endpoint, factory.dataStore) case portainer.EdgeAgentOnKubernetesEnvironment, portainer.AgentOnKubernetesEnvironment, portainer.KubernetesLocalEnvironment: return factory.newKubernetesProxy(endpoint) } diff --git a/api/http/proxy/factory/responseutils/response.go b/api/http/proxy/factory/responseutils/response.go index 1ce1d39e9..cf0596cdd 100644 --- a/api/http/proxy/factory/responseutils/response.go +++ b/api/http/proxy/factory/responseutils/response.go @@ -2,6 +2,7 @@ package responseutils import ( "bytes" + "compress/gzip" "encoding/json" "errors" "io/ioutil" @@ -48,13 +49,21 @@ func getResponseBodyAsGenericJSON(response *http.Response) (interface{}, error) return nil, errors.New("unable to parse response: empty response body") } - var data interface{} - body, err := ioutil.ReadAll(response.Body) - if err != nil { - return nil, err + reader := response.Body + + if response.Header.Get("Content-Encoding") == "gzip" { + response.Header.Del("Content-Encoding") + gzipReader, err := gzip.NewReader(response.Body) + if err != nil { + return nil, err + } + reader = gzipReader } - err = response.Body.Close() + defer reader.Close() + + var data interface{} + body, err := ioutil.ReadAll(reader) if err != nil { return nil, err } diff --git a/api/internal/authorization/access_control.go b/api/internal/authorization/access_control.go index 4f533bffa..5004088db 100644 --- a/api/internal/authorization/access_control.go +++ b/api/internal/authorization/access_control.go @@ -160,9 +160,13 @@ func FilterAuthorizedCustomTemplates(customTemplates []portainer.CustomTemplate, return authorizedTemplates } -// UserCanAccessResource will valide that a user has permissions defined in the specified resource control +// UserCanAccessResource will valid that a user has permissions defined in the specified resource control // based on its identifier and the team(s) he is part of. func UserCanAccessResource(userID portainer.UserID, userTeamIDs []portainer.TeamID, resourceControl *portainer.ResourceControl) bool { + if resourceControl == nil { + return false + } + for _, authorizedUserAccess := range resourceControl.UserAccesses { if userID == authorizedUserAccess.UserID { return true diff --git a/api/portainer.go b/api/portainer.go index 1647e0846..ec8262804 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -1495,6 +1495,8 @@ const ( ConfigResourceControl // CustomTemplateResourceControl represents a resource control associated to a custom template CustomTemplateResourceControl + // ContainerGroupResourceControl represents a resource control associated to an Azure container group + ContainerGroupResourceControl ) const ( @@ -1771,3 +1773,8 @@ const ( EndpointResourcesAccess Authorization = "EndpointResourcesAccess" ) + +const ( + AzurePathContainerGroups = "/subscriptions/*/providers/Microsoft.ContainerInstance/containerGroups" + AzurePathContainerGroup = "/subscriptions/*/resourceGroups/*/providers/Microsoft.ContainerInstance/containerGroups/*" +) diff --git a/app/azure/components/datatables/containergroups-datatable/containerGroupsDatatable.html b/app/azure/components/datatables/containergroups-datatable/containerGroupsDatatable.html index da68fd7a0..62fa95acf 100644 --- a/app/azure/components/datatables/containergroups-datatable/containerGroupsDatatable.html +++ b/app/azure/components/datatables/containergroups-datatable/containerGroupsDatatable.html @@ -49,6 +49,13 @@ Published Ports + + + Ownership + + + + @@ -70,6 +77,12 @@ - + + + + {{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = $ctrl.RCO.ADMINISTRATORS }} + + Loading... diff --git a/app/azure/components/datatables/containergroups-datatable/containerGroupsDatatable.js b/app/azure/components/datatables/containergroups-datatable/containerGroupsDatatable.js index 8d91518a9..22f77137c 100644 --- a/app/azure/components/datatables/containergroups-datatable/containerGroupsDatatable.js +++ b/app/azure/components/datatables/containergroups-datatable/containerGroupsDatatable.js @@ -2,7 +2,7 @@ angular.module('portainer.azure').component('containergroupsDatatable', { templateUrl: './containerGroupsDatatable.html', controller: 'GenericDatatableController', bindings: { - title: '@', + titleText: '@', titleIcon: '@', dataset: '<', tableKey: '@', diff --git a/app/azure/models/container_group.js b/app/azure/models/container_group.js index af2871c92..1c14f2d0d 100644 --- a/app/azure/models/container_group.js +++ b/app/azure/models/container_group.js @@ -1,3 +1,6 @@ +import { AccessControlFormData } from 'Portainer/components/accessControlForm/porAccessControlFormModel'; +import { ResourceControlViewModel } from 'Portainer/models/resourceControl/resourceControl'; + export function ContainerGroupDefaultModel() { this.Location = ''; this.OSType = 'Linux'; @@ -13,6 +16,7 @@ export function ContainerGroupDefaultModel() { ]; this.CPU = 1; this.Memory = 1; + this.AccessControlData = new AccessControlFormData(); } export function ContainerGroupViewModel(data) { @@ -30,6 +34,10 @@ export function ContainerGroupViewModel(data) { this.AllocatePublicIP = data.properties.ipAddress.type === 'Public'; this.CPU = container.properties.resources.requests.cpu; this.Memory = container.properties.resources.requests.memoryInGB; + + if (data.Portainer && data.Portainer.ResourceControl) { + this.ResourceControl = new ResourceControlViewModel(data.Portainer.ResourceControl); + } } export function CreateContainerGroupRequest(model) { diff --git a/app/azure/views/containerinstances/container-instance-details/containerInstanceDetails.html b/app/azure/views/containerinstances/container-instance-details/containerInstanceDetails.html index 37940a149..1b564bd9b 100644 --- a/app/azure/views/containerinstances/container-instance-details/containerInstanceDetails.html +++ b/app/azure/views/containerinstances/container-instance-details/containerInstanceDetails.html @@ -131,4 +131,8 @@ + + + + diff --git a/app/azure/views/containerinstances/create/createContainerInstanceController.js b/app/azure/views/containerinstances/create/createContainerInstanceController.js index cc42380d5..7d73a4477 100644 --- a/app/azure/views/containerinstances/create/createContainerInstanceController.js +++ b/app/azure/views/containerinstances/create/createContainerInstanceController.js @@ -6,7 +6,9 @@ angular.module('portainer.azure').controller('AzureCreateContainerInstanceContro '$state', 'AzureService', 'Notifications', - function ($q, $scope, $state, AzureService, Notifications) { + 'Authentication', + 'ResourceControlService', + function ($q, $scope, $state, AzureService, Notifications, Authentication, ResourceControlService) { var allResourceGroups = []; var allProviders = []; @@ -42,12 +44,12 @@ angular.module('portainer.azure').controller('AzureCreateContainerInstanceContro $scope.state.actionInProgress = true; AzureService.createContainerGroup(model, subscriptionId, resourceGroupName) - .then(function success() { + .then(applyResourceControl) + .then(() => { Notifications.success('Container successfully created', model.Name); $state.go('azure.containerinstances'); }) .catch(function error(err) { - err = err.data ? err.data.error : err; Notifications.error('Failure', err, 'Unable to create container'); }) .finally(function final() { @@ -55,6 +57,14 @@ angular.module('portainer.azure').controller('AzureCreateContainerInstanceContro }); }; + function applyResourceControl(newResourceGroup) { + const userId = Authentication.getUserDetails().ID; + const resourceControl = newResourceGroup.Portainer.ResourceControl; + const accessControlData = $scope.model.AccessControlData; + + return ResourceControlService.applyResourceControl(userId, accessControlData, resourceControl); + } + function validateForm(model) { if (!model.Ports || !model.Ports.length || model.Ports.every((port) => !port.host || !port.container)) { return 'At least one port binding is required'; @@ -73,7 +83,7 @@ angular.module('portainer.azure').controller('AzureCreateContainerInstanceContro } function initView() { - var model = new ContainerGroupDefaultModel(); + $scope.model = new ContainerGroupDefaultModel(); AzureService.subscriptions() .then(function success(data) { @@ -93,8 +103,6 @@ angular.module('portainer.azure').controller('AzureCreateContainerInstanceContro var containerInstancesProviders = data.containerInstancesProviders; allProviders = containerInstancesProviders; - $scope.model = model; - var selectedSubscription = $scope.state.selectedSubscription; updateResourceGroupsAndLocations(selectedSubscription, resourceGroups, containerInstancesProviders); }) diff --git a/app/azure/views/containerinstances/create/createcontainerinstance.html b/app/azure/views/containerinstances/create/createcontainerinstance.html index 477fb845a..1e1d76424 100644 --- a/app/azure/views/containerinstances/create/createcontainerinstance.html +++ b/app/azure/views/containerinstances/create/createcontainerinstance.html @@ -157,6 +157,9 @@ + + +
Actions diff --git a/app/portainer/models/resourceControl/resourceControlTypes.js b/app/portainer/models/resourceControl/resourceControlTypes.js index 714b7dc02..137ee52a2 100644 --- a/app/portainer/models/resourceControl/resourceControlTypes.js +++ b/app/portainer/models/resourceControl/resourceControlTypes.js @@ -7,6 +7,7 @@ export const ResourceControlTypeString = Object.freeze({ STACK: 'stack', VOLUME: 'volume', CUSTOM_TEMPLATE: 'custom-template', + CONTAINER_GROUP: 'container-group', }); /** @@ -21,4 +22,5 @@ export const ResourceControlTypeInt = Object.freeze({ STACK: 6, CONFIG: 7, CUSTOM_TEMPLATE: 8, + CONTAINER_GROUP: 9, }); diff --git a/app/portainer/services/notifications.js b/app/portainer/services/notifications.js index fdfd85e85..2642dcfbf 100644 --- a/app/portainer/services/notifications.js +++ b/app/portainer/services/notifications.js @@ -26,6 +26,8 @@ angular.module('portainer.app').factory('Notifications', [ msg = e.data.message; } else if (e.data && e.data.content) { msg = e.data.content; + } else if (e.data && e.data.error) { + msg = e.data.error; } else if (e.message) { msg = e.message; } else if (e.err && e.err.data && e.err.data.length > 0 && e.err.data[0].message) { From 5b26ef2036e027c51acb0c808e8926322b2f56b1 Mon Sep 17 00:00:00 2001 From: Felix Han Date: Wed, 14 Apr 2021 16:08:49 +1200 Subject: [PATCH 2/2] feat(ACI): updated function name --- api/http/proxy/factory/azure/containergroup.go | 6 +++--- api/http/proxy/factory/azure/containergroups.go | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/api/http/proxy/factory/azure/containergroup.go b/api/http/proxy/factory/azure/containergroup.go index 4fc0042b9..c3383035c 100644 --- a/api/http/proxy/factory/azure/containergroup.go +++ b/api/http/proxy/factory/azure/containergroup.go @@ -28,7 +28,7 @@ func (transport *Transport) proxyContainerGroupPutRequest(request *http.Request) return response, err } - responseObject, err := responseutils.GetResponseAsJSONOBject(response) + responseObject, err := responseutils.GetResponseAsJSONObject(response) if err != nil { return response, err } @@ -64,7 +64,7 @@ func (transport *Transport) proxyContainerGroupGetRequest(request *http.Request) return response, err } - responseObject, err := responseutils.GetResponseAsJSONOBject(response) + responseObject, err := responseutils.GetResponseAsJSONObject(response) if err != nil { return nil, err } @@ -96,7 +96,7 @@ func (transport *Transport) proxyContainerGroupDeleteRequest(request *http.Reque return response, err } - responseObject, err := responseutils.GetResponseAsJSONOBject(response) + responseObject, err := responseutils.GetResponseAsJSONObject(response) if err != nil { return nil, err } diff --git a/api/http/proxy/factory/azure/containergroups.go b/api/http/proxy/factory/azure/containergroups.go index b13d0c924..ccb441b3b 100644 --- a/api/http/proxy/factory/azure/containergroups.go +++ b/api/http/proxy/factory/azure/containergroups.go @@ -23,7 +23,7 @@ func (transport *Transport) proxyContainerGroupsGetRequest(request *http.Request return nil, err } - responseObject, err := responseutils.GetResponseAsJSONOBject(response) + responseObject, err := responseutils.GetResponseAsJSONObject(response) if err != nil { return nil, err }