GKE V2 Provisioner

rancher/rancher#31221

initial creation of gke driver

create shared google service for v1/v2 driver

wip

all fetch methods for google to shared service

cluster/kubernetes options

ippolicy conditionals

private nodes observer so can force ipaliases true

subnetwork logic

cru-private-cluster for gke

initial values

gke node pools

default configs and service cleanup

node group changes

subnet work

hide private nodes config if not enabled

useIpAliases work

 master authorized network component

loggings

no initial master version

gke node pool fixups

input-cidr component and validation

wip - new np logic

node pool updates

fix ups from launching

edit mode changes

more edit updates

more fix ups

node pool edits

import gke

reset auto-scale

implment cloud credentials in gke v2

Cloud cred changes for gke nice to haves

imp fetch clusters

Implement Shared Subnets

cleanup

Import private cluster work and other fixes

private cluster changes

More import/register changes

Null values and node pool version changes

gke private networks warning

fixups
This commit is contained in:
Westly Wright 2021-03-17 16:40:33 -07:00
parent 7070a61bae
commit 657737657c
No known key found for this signature in database
GPG Key ID: 4FAB3D8673DC54A3
39 changed files with 4735 additions and 1516 deletions

View File

@ -70,6 +70,80 @@ export const DEFAULT_EKS_CONFIG = {
type: 'eksclusterconfigspec',
};
export const DEFAULT_GKE_NODE_POOL_CONFIG = {
autoscaling: {
enabled: false,
maxNodeCount: 0,
minNodeCount: 0
},
config: {
diskSizeGb: 100,
diskType: 'pd-standard',
imageType: 'COS',
labels: null,
localSsdCount: 0,
machineType: 'n1-standard-2',
oauthScopes: null,
preemptible: false,
taints: null
},
initialNodeCount: 3,
management: {
autoRepair: false,
autoUpgrade: false,
},
maxPodsConstraint: 110,
name: null,
version: null,
type: 'gkenodepoolconfig'
};
export const DEFAULT_GKE_CONFIG = {
clusterAddons: {
horizontalPodAutoscaling: true,
httpLoadBalancing: true,
networkPolicyConfig: true
},
clusterIpv4Cidr: null,
clusterName: null,
description: null,
enableKubernetesAlpha: false,
googleCredentialSecret: null,
imported: false,
ipAllocationPolicy: {
clusterIpv4CidrBlock: null,
clusterSecondaryRangeName: null,
createSubnetwork: false,
nodeIpv4CidrBlock: null,
servicesIpv4CidrBlock: null,
servicesSecondaryRangeName: null,
subnetworkName: null,
useIpAliases: true
},
kubernetesVersion: '',
locations: null,
loggingService: null,
maintenanceWindow: null,
masterAuthorizedNetworks: {
cidrBlocks: null,
enabled: false
},
monitoringService: null,
network: null,
networkPolicyEnabled: false,
nodePools: [DEFAULT_GKE_NODE_POOL_CONFIG],
privateClusterConfig: {
enablePrivateEndpoint: false,
enablePrivateNodes: false,
masterIpv4CidrBlock: null,
},
projectID: null,
region: 'us-west2',
subnetwork: null,
type: 'gkeclusterconfigspec',
zone: null,
};
export default Resource.extend(Grafana, ResourceUsage, {
globalStore: service(),
@ -219,12 +293,59 @@ export default Resource.extend(Grafana, ResourceUsage, {
return !!actionLinks.saveAsTemplate;
}),
hasPublicAccess: computed('eksConfig.publicAccess', 'eksStatus.upstreamSpec.publicAccess', 'property', function() {
return this?.eksStatus?.upstreamSpec?.publicAccess || this?.eksConfig?.publicAccess || true;
hasPublicAccess: computed('eksConfig.publicAccess', 'eksStatus.upstreamSpec.publicAccess', 'gkeStatus.privateClusterConfig.enablePrivateNodes', 'gkeStatus.upstreamSpec.privateClusterConfig.enablePrivateNodes', function() {
const { clusterProvider } = this;
switch (clusterProvider) {
case 'amazoneksv2':
return this?.eksStatus?.upstreamSpec?.publicAccess || this?.eksConfig?.publicAccess || true;
case 'googlegkev2':
return !this?.gkeStatus?.upstreamSpec?.privateClusterConfig?.enablePrivateNodes || !this?.gkeStatus?.privateClusterConfig?.enablePrivateNodes || true;
default:
return true;
}
}),
hasPrivateAccess: computed('eksConfig.privateAccess', 'eksStatus.upstreamSpec.privateAccess', 'property', function() {
return this?.eksStatus?.upstreamSpec?.privateAccess || this?.eksConfig?.privateAccess || false;
hasPrivateAccess: computed('eksConfig.privateAccess', 'eksStatus.upstreamSpec.privateAccess', 'gkeConfig.privateClusterConfig.enablePrivateNodes', 'gkeStatus.upstreamSpec.privateClusterConfig.enablePrivateNodes', function() {
const { clusterProvider } = this;
switch (clusterProvider) {
case 'amazoneksv2':
return this?.eksStatus?.upstreamSpec?.privateAccess || this?.eksConfig?.privateAccess || false;
case 'googlegkev2':
return this?.gkeStatus?.upstreamSpec?.privateClusterConfig?.enablePrivateNodes || this?.gkeConfig?.privateClusterConfig?.enablePrivateNodes;
default:
return false;
}
}),
displayImportLabel: computed('clusterProvider', 'eksDisplayEksImport', 'gkeDisplayImport', function() {
const { clusterProvider } = this;
switch (clusterProvider) {
case 'amazoneksv2':
return this.eksDisplayEksImport ? true : false;
case 'googlegkev2':
return this.gkeDisplayImport ? true : false;
case 'import':
return true;
default:
return false;
}
}),
gkeDisplayImport: computed('clusterProvider', 'hasPrivateAccess', 'imported', function() {
const { clusterProvider } = this;
if (clusterProvider !== 'googlegkev2') {
return false;
}
if (this.hasPrivateAccess) {
return true;
}
return false;
}),
eksDisplayEksImport: computed('hasPrivateAccess', 'hasPublicAccess', function() {
@ -241,9 +362,9 @@ export default Resource.extend(Grafana, ResourceUsage, {
return false;
}),
canShowAddHost: computed('clusterProvider', 'hasPrivateAccess', 'hasPublicAccess', 'nodes', function() {
canShowAddHost: computed('clusterProvider', 'hasPrivateAccess', 'hasPublicAccess', 'imported', 'nodes', function() {
const { clusterProvider } = this;
const compatibleProviders = ['custom', 'import', 'amazoneksv2'];
const compatibleProviders = ['custom', 'import', 'amazoneksv2', 'googlegkev2'];
const nodes = get(this, 'nodes');
if (!compatibleProviders.includes(clusterProvider)) {
@ -253,7 +374,9 @@ export default Resource.extend(Grafana, ResourceUsage, {
// private access requires the ability to run the import command on the cluster
if (clusterProvider === 'amazoneksv2' && !!this.hasPublicAccess && this.hasPrivateAccess) {
return true;
} else if (clusterProvider !== 'amazoneksv2' && isEmpty(nodes)) {
} else if (clusterProvider === 'googlev2' && this.hasPrivateAccess) {
return true;
} else if (( clusterProvider !== 'amazoneksv2' || clusterProvider !== 'googlegkev2') && isEmpty(nodes)) {
return true;
}
@ -302,6 +425,8 @@ export default Resource.extend(Grafana, ResourceUsage, {
return 'amazoneksv2';
case 'azureKubernetesServiceConfig':
return 'azureaks';
case 'gkeConfig':
return 'googlegkev2';
case 'googleKubernetesEngineConfig':
return 'googlegke';
case 'tencentEngineConfig':
@ -480,7 +605,7 @@ export default Resource.extend(Grafana, ResourceUsage, {
return false;
}),
availableActions: computed('actionLinks.{rotateCertificates,rotateEncryptionKey}', 'canRotateEncryptionKey', 'canSaveAsTemplate', 'canShowAddHost', 'eksDisplayEksImport', 'isClusterScanDisabled', function() {
availableActions: computed('actionLinks.{rotateCertificates,rotateEncryptionKey}', 'canRotateEncryptionKey', 'canSaveAsTemplate', 'canShowAddHost', 'displayImportLabel', 'isClusterScanDisabled', function() {
const a = get(this, 'actionLinks') || {};
return [
@ -516,7 +641,7 @@ export default Resource.extend(Grafana, ResourceUsage, {
enabled: this.canSaveAsTemplate,
},
{
label: this.eksDisplayEksImport ? 'action.importHost' : 'action.registration',
label: this.displayImportLabel ? 'action.importHost' : 'action.registration',
icon: 'icon icon-host',
action: 'showCommandModal',
enabled: this.canShowAddHost,
@ -700,7 +825,7 @@ export default Resource.extend(Grafana, ResourceUsage, {
}
};
if (provider === 'import' && isEmpty(get(this, 'eksConfig'))) {
if (provider === 'import' && isEmpty(get(this, 'eksConfig') && isEmpty(get(this, 'gkeConfig')))) {
set(queryParams, 'queryParams.importProvider', 'other');
}
@ -708,6 +833,10 @@ export default Resource.extend(Grafana, ResourceUsage, {
set(queryParams, 'queryParams.provider', 'amazoneksv2');
}
if (provider === 'gke' && !isEmpty(get(this, 'gkeConfig'))) {
set(queryParams, 'queryParams.provider', 'googlegkev2');
}
if (this.clusterTemplateRevisionId) {
set(queryParams, 'queryParams.clusterTemplateRevision', this.clusterTemplateRevisionId);
}
@ -847,99 +976,167 @@ export default Resource.extend(Grafana, ResourceUsage, {
},
save(opt) {
const { globalStore, eksConfig } = this;
const { eksConfig, gkeConfig } = this;
if (get(this, 'driver') === 'EKS' || (this.isObject(get(this, 'eksConfig')) && !this.isEmptyObject(get(this, 'eksConfig')))) {
const options = ({
...opt,
data: {
name: this.name,
eksConfig: {},
}
});
const eksClusterConfigSpec = globalStore.getById('schema', 'eksclusterconfigspec');
const nodeGroupConfigSpec = globalStore.getById('schema', 'nodegroup');
if (isEmpty(this.id)) {
sanitizeConfigs(eksClusterConfigSpec, nodeGroupConfigSpec);
if (!get(this, 'eksConfig.imported') && this.name !== get(this, 'eksConfig.displayName')) {
set(this, 'eksConfig.displayName', this.name);
}
return this._super(...arguments);
} else {
const config = jsondiffpatch.clone(get(this, 'eksConfig'));
const upstreamSpec = jsondiffpatch.clone(get(this, 'eksStatus.upstreamSpec'));
if (isEmpty(upstreamSpec)) {
sanitizeConfigs(eksClusterConfigSpec, nodeGroupConfigSpec);
return this._super(...arguments);
}
set(options, 'data.eksConfig', this.diffEksUpstream(upstreamSpec, config));
if (!isEmpty(get(options, 'data.eksConfig.nodeGroups'))) {
get(options, 'data.eksConfig.nodeGroups').forEach((ng) => {
replaceNullWithEmptyDefaults(ng, get(nodeGroupConfigSpec, 'resourceFields'));
});
}
if (get(options, 'qp._replace')) {
delete options.qp['_replace'];
}
return this._super(options);
}
} else {
return this._super(...arguments);
if (get(this, 'driver') === 'EKS' || (this.isObject(eksConfig) && !this.isEmptyObject(eksConfig))) {
this.syncEksConfigs(opt);
} else if (this.isObject(gkeConfig) && !this.isEmptyObject(gkeConfig)) {
this.syncGkeConfigs(opt);
}
function sanitizeConfigs(eksClusterConfigSpec, nodeGroupConfigSpec) {
replaceNullWithEmptyDefaults(eksConfig, get(eksClusterConfigSpec, 'resourceFields'));
return this._super(...arguments);
},
if (!isEmpty(get(eksConfig, 'nodeGroups'))) {
get(eksConfig, 'nodeGroups').forEach((ng) => {
replaceNullWithEmptyDefaults(ng, get(nodeGroupConfigSpec, 'resourceFields'));
syncGkeConfigs(opt) {
const {
gkeConfig, globalStore, id
} = this;
const options = ({
...opt,
data: {
name: this.name,
gkeConfig: {},
}
});
const gkeClusterConfigSpec = globalStore.getById('schema', 'gkeclusterconfigspec');
const gkeNodePoolConfigSpec = globalStore.getById('schema', 'gkenodepoolconfig'); // will be gkeNodeConfig
if (isEmpty(id)) {
this.sanitizeConfigs(gkeConfig, gkeClusterConfigSpec, gkeNodePoolConfigSpec, 'nodePools');
if (!get(this, 'gkeConfig.imported') && this.name !== get(this, 'gkeConfig.clusterName')) {
set(this, 'gkeConfig.clusterName', this.name);
}
return;
} else {
const config = jsondiffpatch.clone(gkeConfig);
const upstreamSpec = jsondiffpatch.clone(get(this, 'gkeStatus.upstreamSpec'));
if (isEmpty(upstreamSpec)) {
this.sanitizeConfigs(gkeConfig, gkeClusterConfigSpec, gkeNodePoolConfigSpec, 'nodePools');
return;
}
set(options, 'data.gkeConfig', this.diffUpstreamSpec(upstreamSpec, config));
if (!isEmpty(get(options, 'data.gkeConfig.nodeGroups'))) {
get(options, 'data.gkeConfig.nodeGroups').forEach((ng) => {
this.replaceNullWithEmptyDefaults(ng, get(gkeNodePoolConfigSpec, 'resourceFields'));
});
}
if (get(options, 'qp._replace')) {
delete options.qp['_replace'];
}
return;
}
},
function replaceNullWithEmptyDefaults(config, resourceFields) {
Object.keys(config).forEach((ck) => {
const configValue = get(config, ck);
syncEksConfigs(opt) {
const { eksConfig, globalStore, } = this;
if (configValue === null || typeof configValue === 'undefined') {
const resourceField = resourceFields[ck];
const options = ({
...opt,
data: {
name: this.name,
eksConfig: {},
}
});
if (resourceField.type === 'string') {
set(config, ck, '');
} else if (resourceField.type.includes('array')) {
set(config, ck, []);
} else if (resourceField.type.includes('map')) {
set(config, ck, {});
} else if (resourceField.type.includes('boolean')) {
if (resourceField.default) {
set(config, ck, resourceField.default);
} else {
const eksClusterConfigSpec = globalStore.getById('schema', 'eksclusterconfigspec');
const nodeGroupConfigSpec = globalStore.getById('schema', 'nodegroup');
if (isEmpty(this.id)) {
this.sanitizeConfigs(eksConfig, eksClusterConfigSpec, nodeGroupConfigSpec);
if (!get(this, 'eksConfig.imported') && this.name !== get(this, 'eksConfig.displayName')) {
set(this, 'eksConfig.displayName', this.name);
}
return;
} else {
const config = jsondiffpatch.clone(get(this, 'eksConfig'));
const upstreamSpec = jsondiffpatch.clone(get(this, 'eksStatus.upstreamSpec'));
if (isEmpty(upstreamSpec)) {
this.sanitizeConfigs(eksConfig, eksClusterConfigSpec, nodeGroupConfigSpec);
return;
}
set(options, 'data.eksConfig', this.diffUpstreamSpec(upstreamSpec, config));
if (!isEmpty(get(options, 'data.eksConfig.nodeGroups'))) {
get(options, 'data.eksConfig.nodeGroups').forEach((ng) => {
this.replaceNullWithEmptyDefaults(ng, get(nodeGroupConfigSpec, 'resourceFields'));
});
}
if (get(options, 'qp._replace')) {
delete options.qp['_replace'];
}
return;
}
},
sanitizeConfigs(currentConfig, clusterConfigSpec, nodeGroupConfigSpec, nodeType = 'nodeGroups') {
this.replaceNullWithEmptyDefaults(currentConfig, get(clusterConfigSpec, 'resourceFields'));
if (!isEmpty(get(currentConfig, nodeType))) {
get(currentConfig, nodeType).forEach((ng) => {
this.replaceNullWithEmptyDefaults(ng, get(nodeGroupConfigSpec, 'resourceFields'));
});
}
},
replaceNullWithEmptyDefaults(config, resourceFields) {
const { clusterProvider } = this;
Object.keys(config).forEach((ck) => {
const configValue = get(config, ck);
if (configValue === null || typeof configValue === 'undefined') {
const resourceField = resourceFields[ck];
if (resourceField.type === 'string') {
set(config, ck, '');
} else if (resourceField.type.includes('array')) {
set(config, ck, []);
} else if (resourceField.type.includes('map')) {
set(config, ck, {});
} else if (resourceField.type.includes('boolean')) {
if (resourceField.default) {
set(config, ck, resourceField.default);
} else {
// we shouldn't get here, there are not that many fields in EKS and I've set the defaults for bools that are there
// but if we do hit this branch my some magic case imo a bool isn't something we can default cause its unknown...just dont do anything.
if (clusterProvider === 'amazoneksv2') {
if ( !isEmpty(get(DEFAULT_EKS_CONFIG, ck)) || !isEmpty(get(DEFAULT_NODE_GROUP_CONFIG, ck)) ) {
let match = isEmpty(get(DEFAULT_EKS_CONFIG, ck)) ? get(DEFAULT_NODE_GROUP_CONFIG, ck) : get(DEFAULT_EKS_CONFIG, ck);
set(config, ck, match);
}
// we shouldn't get here, there are not that many fields in EKS and I've set the defaults for bools that are there
// but if we do hit this branch my some magic case imo a bool isn't something we can default cause its unknown...just dont do anything.
} else if (clusterProvider === 'googlegkev2') {
if ( !isEmpty(get(DEFAULT_GKE_CONFIG, ck)) || !isEmpty(get(DEFAULT_GKE_NODE_POOL_CONFIG, ck)) ) {
let match = isEmpty(get(DEFAULT_GKE_CONFIG, ck)) ? get(DEFAULT_GKE_NODE_POOL_CONFIG, ck) : get(DEFAULT_GKE_CONFIG, ck);
set(config, ck, match);
}
}
}
}
});
return config;
}
}
});
},
diffEksUpstream(lhs, rhs) {
diffUpstreamSpec(lhs, rhs) {
// this is NOT a generic object diff.
// It tries to be as generic as possible but it does make certain assumptions regarding nulls and emtpy arrays/objects
// if LHS (upstream) is null and RHS (eks config) is empty we do not count this as a change
@ -961,7 +1158,7 @@ export default Resource.extend(Grafana, ResourceUsage, {
}
} catch (e){}
if (k === 'nodeGroups' || k === 'tags') {
if (k === 'nodeGroups' || k === 'nodePools' || k === 'tags') {
if (!isEmpty(rhsMatch)) {
// node groups need ALL data so short circut and send it all
set(delta, k, rhsMatch);
@ -1012,7 +1209,7 @@ export default Resource.extend(Grafana, ResourceUsage, {
if (lMatch) {
// we have a match in the upstream, meaning we've probably made updates to the object itself
const diffedMatch = this.diffEksUpstream(lMatch, match);
const diffedMatch = this.diffUpstreamSpec(lMatch, match);
if (!isArray(get(delta, k))) {
set(delta, k, [diffedMatch]);
@ -1047,7 +1244,7 @@ export default Resource.extend(Grafana, ResourceUsage, {
if (!isEmpty(rhsMatch) && !this.isEmptyObject(rhsMatch)) {
if ((Object.keys(lhsMatch) || []).length > 0) {
// You have more diffing to do
set(delta, k, this.diffEksUpstream(lhsMatch, rhsMatch));
set(delta, k, this.diffUpstreamSpec(lhsMatch, rhsMatch));
} else if (this.isEmptyObject(lhsMatch)) {
// we had a map now we have an empty map
set(delta, k, {});

View File

@ -378,3 +378,11 @@ input.input-lg,
}
}
}
.input-cidr-container {
position: relative;
.error-container {
position: absolute;
}
}

View File

@ -69,6 +69,7 @@
&.amazoneksv2 { @include amazoneks; }
&.azureaks { @include azureaks; }
&.googlegke { @include googlegke; }
&.googlegkev2 { @include googlegke; }
&.tencenttke { @include tencenttke; }
&.huaweicce { @include huaweicce; }
&.oracleoke { @include oracleoke; }

View File

@ -71,6 +71,15 @@ export default Controller.extend({
driver: 'googlegke',
kontainerId: 'googlekubernetesengine',
},
{
displayName: 'Google GKE',
driver: 'gke',
name: 'googlegkev2',
nodePool: false,
nodeWhich: 'gke',
preSave: false,
postSave: true,
},
{
name: 'amazoneks',
driver: 'amazoneks',
@ -112,7 +121,7 @@ export default Controller.extend({
},
];
out = out.filter( (o) => builtIn.findBy('id', o.kontainerId) || o.name === 'amazoneksv2' );
out = out.filter( (o) => builtIn.findBy('id', o.kontainerId) || o.name === 'amazoneksv2' || o.name === 'googlegkev2' );
if (custom.length > 0) {
custom.forEach( (c) => {

View File

@ -23,6 +23,18 @@
</p>
</div>
</a>
<a
class="col span-2 cluster-driver-box small span-height"
href="{{href-to "global-admin.clusters.new.launch" "import" (query-params importProvider='gke')}}"
disabled={{disabledAddCluster}}
>
<div class="machine-driver googlegke"></div>
<div class="driver-content">
<p class="driver-name">
{{t "clusterNew.googlegke.shortLabel"}}
</p>
</div>
</a>
<a
class="col span-2 cluster-driver-box small span-height"
href="{{href-to "global-admin.clusters.new.launch" "import" (query-params importProvider='other')}}"
@ -126,7 +138,7 @@
</div>
{{#each (get providerGroups "cloudGroup") as |provider|}}
{{#unless provider.scriptError}}
{{#unless (eq provider.name "amazoneks")}}
{{#unless (or (eq provider.name "amazoneks") (eq provider.name "googlegke"))}}
{{#link-to
"clusters.new.launch"
provider.name

View File

@ -7,6 +7,8 @@
{{else if (eq provider "import")}}
{{#if (eq router.currentRoute.queryParams.importProvider "eks")}}
{{t "clustersPage.launch.importEks"}}
{{else if (eq router.currentRoute.queryParams.importProvider "gke")}}
{{t "clustersPage.launch.importGke"}}
{{else}}
{{t "clustersPage.launch.import"}}
{{/if}}

View File

@ -65,6 +65,7 @@ export default Component.extend(ViewNewEdit, {
linode: service(),
oci: service(),
intl: service(),
google: service(),
layout,
nodeConfigTemplateType: null,
cloudCredentialType: null,
@ -81,6 +82,7 @@ export default Component.extend(ViewNewEdit, {
urlInvalid: false,
urlWarning: null,
urlError: null,
gkeProjectId: null,
init() {
this._super(...arguments);
@ -125,7 +127,7 @@ export default Component.extend(ViewNewEdit, {
},
},
config: computed('cloudCredentialType', 'model.{amazonec2credentialConfig,azurecredentialConfig,digitaloceancredentialConfig,linodecredentialConfig,ocicredentialConfig,pnapcredentialConfig,vmwarevspherecredentialConfig}', function() {
config: computed('cloudCredentialType', 'model.{amazonec2credentialConfig,azurecredentialConfig,digitaloceancredentialConfig,googlecredentialConfig,linodecredentialConfig,ocicredentialConfig,pnapcredentialConfig,vmwarevspherecredentialConfig}', function() {
const { model } = this;
const configField = this.getConfigField();
@ -278,6 +280,10 @@ export default Component.extend(ViewNewEdit, {
if (cloudCredentialType === 'google') {
return this.fetchZones().then(() => {
const auth = JSON.parse(get(this, 'config.authEncodedJson'));
const projectId = auth?.project_id;
set(this, 'gkeProjectId', projectId);
set(this, 'validatingKeys', false);
return true;
@ -312,9 +318,17 @@ export default Component.extend(ViewNewEdit, {
},
doneSaving(neu) {
// API sends back empty object which doesn't overrite the keys when the response is merged.
// Just need to ensure that when the user loads this model again the acceess key/secret/pw is not present.
set(neu, this.getConfigField(), {});
const driverName = get(this, 'driverName');
const projectId = get(this, 'gkeProjectId');
if (driverName === 'google' && projectId) {
set(neu, this.getConfigField(), { projectId });
set(this, 'gkeProjectId', null)
} else {
// API sends back empty object which doesn't overrite the keys when the response is merged.
// Just need to ensure that when the user loads this model again the acceess key/secret/pw is not present.
set(neu, this.getConfigField(), {});
}
this.model.replaceWith(neu);

View File

@ -0,0 +1,660 @@
import { isArray } from '@ember/array';
import Component from '@ember/component';
import {
computed, get, observer, set, setProperties
} from '@ember/object';
import { alias, equal, union } from '@ember/object/computed';
import { next } from '@ember/runloop';
import { inject as service } from '@ember/service';
import { isEmpty } from '@ember/utils';
import { all } from 'rsvp';
import ClusterDriver from 'shared/mixins/cluster-driver';
import { sortableNumericSuffix } from 'shared/utils/util';
import { DEFAULT_GKE_CONFIG, DEFAULT_GKE_NODE_POOL_CONFIG } from 'ui/models/cluster';
import layout from './template';
export default Component.extend(ClusterDriver, {
google: service(),
intl: service(),
serivceVersions: service('version-choices'),
layout,
configField: 'gkeConfig',
step: 1,
errors: null,
otherErrors: null,
clusterErrors: null,
save: false,
clusterAdvanced: false,
maintenanceWindowTimes: null,
locationType: null,
monitoringServiceChoices: null,
loggingServiceChoices: null,
sharedSubnets: null,
allErrors: union('errors', 'otherErrors', 'clusterErrors'),
isNew: equal('mode', 'new'),
editing: equal('mode', 'edit'),
clusterState: alias('model.originalCluster.state'),
init() {
this._super(...arguments);
setProperties(this, {
errors: [],
otherErrors: [],
clusterErrors: [],
maintenanceWindowTimes: this.google.maintenanceWindows,
locationType: this.google.defaultZoneType,
monitoringServiceChoices: [
{
label: this.intl.t('generic.none'),
value: 'none'
},
{
label: this.intl.t('clusterNew.googlegke.monitoringService.default'),
value: 'monitoring.googleapis.com/kubernetes'
},
],
loggingServiceChoices: [
{
label: this.intl.t('generic.none'),
value: 'none'
},
{
label: this.intl.t('clusterNew.googlegke.loggingService.default'),
value: 'logging.googleapis.com/kubernetes'
},
],
});
let config = get(this, 'cluster.gkeConfig');
if (!config) {
config = this.globalStore.createRecord(this.defaultConfig());
set(this, 'cluster.gkeConfig', config);
}
setProperties(this, {
locationType: get(this, 'config.zone') ? this.google.defaultZoneType : this.google.defaultRegionType,
regionChoices: this.google.regions.map((region) => ({ name: region })),
});
},
actions: {
finishAndSelectCloudCredential(cred) {
if (cred) {
next(this, () => {
set(this, 'config.googleCredentialSecret', get(cred, 'id'));
if (cred?.googlecredentialConfig?.projectId) {
set(this, 'config.projectID', cred.googlecredentialConfig.projectId);
this.send('loadZones');
}
})
}
},
addNodePool() {
let { config } = this;
let { nodePools = [], kubernetesVersion } = config;
const npConfig = { ...DEFAULT_GKE_NODE_POOL_CONFIG };
if (!isArray(nodePools)) {
nodePools = [];
}
const nodeGroup = this.globalStore.createRecord(npConfig);
set(nodeGroup, 'version', kubernetesVersion);
nodePools.pushObject(nodeGroup);
set(this, 'config.nodePools', nodePools);
},
removeNodePool(nodePool) {
let { config: { nodePools = [] } } = this;
if (!isEmpty(nodePools)) {
nodePools.removeObject(nodePool);
}
set(this, 'config.nodePools', nodePools);
},
loadZones(cb = () => {}) {
set(this, 'errors', []);
const config = get(this, `cluster.${ this.configField }`);
return all([
this.google.fetchZones(config, this.saved),
]).then((resp) => {
const [zones] = resp;
setProperties(this, {
step: 2,
zones,
})
cb(true);
}).catch((err) => {
this.send('errorHandler', err);
cb(false);
});
},
checkServiceAccount(cb) {
set(this, 'errors', []);
const config = get(this, `cluster.${ this.configField }`);
return all([
this.google.fetchVersions(config, this.saved),
this.google.fetchMachineTypes(config, this.saved),
this.google.fetchNetworks(config, this.saved),
this.google.fetchSubnetworks(config, get(this, 'locationType'), this.saved),
this.google.fetchSharedSubnets(config, this.saved),
this.google.fetchServiceAccounts(config, this.saved),
]).then((resp) => {
const [versions, machineTypes, networks, subNetworks, sharedSubnets, servicesAccounts] = resp;
setProperties(this, {
step: 3,
subNetworks,
machineTypes,
networks,
servicesAccounts,
sharedSubnets,
versions,
})
// const filter = servicesAccounts.filter((o) => o.displayName === 'Compute Engine default service account')
if (get(this, 'mode') === 'new') {
// set(this, 'config.serviceAccount', filter?.firstObject && filter.firstObject.uniqueId)
set(this, 'config.network', networks?.firstObject && networks.firstObject.name)
}
if (isEmpty(config.kubernetesVersion)) {
set(this, 'config.kubernetesVersion', versions?.defaultClusterVersion);
}
cb(true);
}).catch((err) => {
this.send('errorHandler', err);
cb(false);
});
},
addMSAN() {
const cidrBlocks = (this.config?.masterAuthorizedNetworks?.cidrBlocks ?? []).slice();
cidrBlocks.pushObject(this.globalStore.createRecord({
cidrBlock: '',
displayName: '',
type: 'cidrblock',
}));
set(this, 'config.masterAuthorizedNetworks.cidrBlocks', cidrBlocks);
},
removeMSAN(MSAN) {
const cidrBlocks = this.config?.masterAuthorizedNetworks?.cidrBlocks.slice() ?? [];
cidrBlocks.removeObject(MSAN);
set(this, 'config.masterAuthorizedNetworks.cidrBlocks', cidrBlocks);
},
},
clusterLocationChanged: observer('locationType', function() {
const { locationType } = this;
if (locationType === 'regional') {
setProperties(this, {
'config.zone': null,
'config.locations': [],
});
} else {
set(this, 'config.region', null);
}
}),
clusterSecondaryRangeNameChanged: observer('config.ipAllocationPolicy.clusterSecondaryRangeName', function() {
if (this.isDestroyed || this.isDestroying || this.saving) {
return;
}
const clusterSecondaryRangeName = get(this, 'config.ipAllocationPolicy.clusterSecondaryRangeName');
const secondaryIpRangeContent = get(this, 'secondaryIpRangeContent') || [];
const rangeMatch = secondaryIpRangeContent.findBy('value', clusterSecondaryRangeName);
if (isEmpty(rangeMatch)) {
if (!(isEmpty(get(this, 'config.ipAllocationPolicy.clusterIpv4CidrBlock')))) {
set(this, 'config.ipAllocationPolicy.clusterIpv4CidrBlock', null);
}
} else {
set(this, 'config.ipAllocationPolicy.clusterIpv4CidrBlock', rangeMatch.ipCidrRange);
}
}),
enablePrivateNodes: observer('config.privateClusterConfig.enablePrivateNodes', 'config.ipAllocationPolicy.useIpAliases', function() {
if (this.isDestroyed || this.isDestroying || this.saving) {
return;
}
const { config } = this;
const { privateClusterConfig: { enablePrivateNodes = false }, } = config ?? {};
if (enablePrivateNodes) {
setProperties(config, {
'ipAllocationPolicy.useIpAliases': true,
'masterAuthorizedNetworks.enabled': true,
});
}
}),
networkChange: observer('config.network', 'subNetworkContent.[]', function() {
if (this.isDestroyed || this.isDestroying || this.saving) {
return;
}
const subNetworkContent = get(this, 'subNetworkContent') || []
if (subNetworkContent.length >= 1) {
const firstNonNullSubnetMatch = subNetworkContent.find((sn) => !isEmpty(sn.value));
setProperties(this, {
'config.subnetwork': firstNonNullSubnetMatch?.value || '',
'config.ipAllocationPolicy.createSubnetwork': false,
'config.ipAllocationPolicy.subnetworkName': null,
'config.ipAllocationPolicy.nodeIpv4CidrBlock': null,
});
const secondaryIpRangeContent = get(this, 'secondaryIpRangeContent') || []
if (secondaryIpRangeContent.length > 0) {
const value = secondaryIpRangeContent[0] && secondaryIpRangeContent[0].value
setProperties(this, {
'config.ipAllocationPolicy.clusterSecondaryRangeName': value,
'config.ipAllocationPolicy.servicesSecondaryRangeName': value,
});
} else {
setProperties(this, {
'config.ipAllocationPolicy.clusterSecondaryRangeName': null,
'config.ipAllocationPolicy.servicesSecondaryRangeName': null,
});
}
} else {
setProperties(this, {
'config.subnetwork': '',
'config.ipAllocationPolicy.createSubnetwork': true,
'config.ipAllocationPolicy.clusterSecondaryRangeName': null,
'config.ipAllocationPolicy.servicesSecondaryRangeName': null,
});
}
}),
servicesSecondaryRangeNameChanged: observer('config.ipAllocationPolicy.servicesSecondaryRangeName', function() {
if (this.isDestroyed || this.isDestroying || this.saving) {
return;
}
const servicesSecondaryRangeName = get(this, 'config.ipAllocationPolicy.servicesSecondaryRangeName');
const secondaryIpRangeContent = get(this, 'secondaryIpRangeContent') || [];
const rangeMatch = secondaryIpRangeContent.findBy('value', servicesSecondaryRangeName);
if (isEmpty(rangeMatch)) {
if (!isEmpty(get(this, 'config.ipAllocationPolicy.servicesIpv4CidrBlock'))) {
set(this, 'config.ipAllocationPolicy.servicesIpv4CidrBlock', null);
}
} else {
set(this, 'config.ipAllocationPolicy.servicesIpv4CidrBlock', rangeMatch.ipCidrRange);
}
}),
secondaryIpRangeContentChange: observer('secondaryIpRangeContent.[]', 'config.ipAllocationPolicy.useIpAliases', function() {
if (this.isDestroyed || this.isDestroying || this.saving) {
return;
}
const secondaryIpRangeContent = get(this, 'secondaryIpRangeContent') || []
if (secondaryIpRangeContent.length === 0) {
setProperties(this, {
'config.ipAllocationPolicy.createSubnetwork': true,
'config.ipAllocationPolicy.clusterSecondaryRangeName': null,
'config.ipAllocationPolicy.servicesSecondaryRangeName': null,
});
}
}),
subnetworkChange: observer('config.subnetwork', function() {
if (this.isDestroyed || this.isDestroying || this.saving) {
return;
}
const { config: { subnetwork } } = this;
if (isEmpty(subnetwork)) {
setProperties(this, {
'config.ipAllocationPolicy.createSubnetwork': true,
'config.ipAllocationPolicy.clusterSecondaryRangeName': null,
'config.ipAllocationPolicy.servicesSecondaryRangeName': null,
});
} else {
setProperties(this, {
'config.ipAllocationPolicy.createSubnetwork': false,
'config.ipAllocationPolicy.subnetworkName': null,
'config.ipAllocationPolicy.nodeIpv4CidrBlock': null,
});
}
}),
useIpAliasesChanged: observer('config.ipAllocationPolicy.useIpAliases', function() {
if (this.isDestroyed || this.isDestroying || this.saving) {
return;
}
const useIpAliases = get(this, 'config.ipAllocationPolicy.useIpAliases');
if (useIpAliases) {
if (!isEmpty(this.config.subnetwork)) {
set(this, 'config.ipPolicyCreateSubnetwork', false);
}
} else {
setProperties(this, {
'config.ipAllocationPolicy.clusterSecondaryRangeName': null,
'config.ipAllocationPolicy.servicesSecondaryRangeName': null,
});
}
}),
postSaveChanged: observer('isPostSave', function() {
const {
isNew,
isPostSave,
config: { privateClusterConfig: { enablePrivateNodes } }
} = this;
if (enablePrivateNodes && isPostSave) {
if (isNew) {
set(this, 'step', 5);
} else {
this.close();
}
} else {
this.close();
}
}),
importedClusterIsPending: computed('clusterIsPending', 'model.originalCluster', function() {
const { clusterIsPending } = this;
const originalCluster = get(this, 'model.originalCluster');
const ourClusterSpec = get(( originalCluster ?? {} ), 'gkeConfig');
const upstreamSpec = get(( originalCluster ?? {} ), 'gkeStatus.upstreamSpec');
return clusterIsPending && get(ourClusterSpec, 'imported') && !isEmpty(upstreamSpec);
}),
clusterIsPending: computed('clusterState', function() {
const { clusterState } = this;
return ['pending', 'provisioning', 'waiting'].includes(clusterState);
}),
cloudCredentials: computed('model.cloudCredentials', function() {
const { model: { cloudCredentials } } = this;
return cloudCredentials.filter((cc) => Object.prototype.hasOwnProperty.call(cc, 'googlecredentialConfig'));
}),
disableSecondaryRangeNames: computed('config.ipAllocationPolicy.{createSubnetwork,useIpAliases}', function() {
const ipAllocationPolicy = get(this, 'config.ipAllocationPolicy');
const { createSubnetwork = false, useIpAliases = false } = ipAllocationPolicy ?? {
createSubnetwork: false,
useIpAliases: false
};
if (!useIpAliases && !createSubnetwork) {
return true;
}
if (useIpAliases && !createSubnetwork) {
return false;
}
return true;
}),
locationContent: computed('config.zone', 'zoneChoices', function() {
const zone = get(this, 'config.zone')
if ( !zone ) {
return [];
}
const arr = zone.split('-')
const locationName = `${ arr[0] }-${ arr[1] }`
const zoneChoices = get(this, 'zoneChoices')
return zoneChoices.filter((z) => (z.name || '').startsWith(locationName) && z.name !== zone)
}),
maintenanceWindowChoice: computed('maintenanceWindowTimes.[]', 'config.maintenanceWindow', function() {
return get(this, 'maintenanceWindowTimes').findBy('value', get(this, 'config.maintenanceWindow')) || { label: 'Any Time' };
}),
networkContent: computed('config.zone', 'networks', 'sharedSubnets', function() {
const networks = (get(this, 'networks') || []).map((net) => {
return {
...net,
group: 'VPC',
shared: false
};
});
const sharedSubnets = (get(this, 'sharedSubnets') || []).map((ssn) => {
return {
...ssn,
group: 'Shared VPC',
shared: true,
name: ssn?.network,
};
})
const merged = [...networks, ...sharedSubnets];
return merged;
}),
secondaryIpRangeContent: computed('subNetworkContent.[]', 'config.network', function() {
const { subNetworkContent = [], config: { network } } = this;
const subNetwork = subNetworkContent.findBy('networkDisplayName', network);
if (subNetwork) {
const { secondaryIpRanges = [] } = subNetwork;
return secondaryIpRanges.map((s) => {
return {
label: `${ s.rangeName }(${ s.ipCidrRange })`,
value: s.rangeName,
ipCidrRange: s.ipCidrRange,
}
});
}
return [];
}),
subNetworkContent: computed('subNetworks.[]', 'sharedSubnets.[]', 'config.network', 'config.zone', function() {
const {
config: { network: networkName },
networkContent,
subNetworks = [],
sharedSubnets = [],
} = this;
const networkMatch = networkContent.findBy('name', networkName);
let filteredSubnets = [];
let mappedSubnets = [];
let out = [];
if (!isEmpty(networkMatch) && networkMatch.shared) {
const sharedVpcs = sharedSubnets.filterBy('network', networkName);
mappedSubnets = sharedVpcs.map((sVpc) => {
const networkDisplayName = sVpc.network;
return {
label: `${ sVpc.subnetwork } (${ sVpc.ipCidrRange })`,
value: sVpc.subnetwork,
secondaryIpRanges: sVpc.secondaryIpRanges,
networkDisplayName
}
});
out = [...mappedSubnets];
} else {
filteredSubnets = (subNetworks || []).filter((s) => {
const network = networkContent.findBy('selfLink', s.network);
const networkDisplayName = network.name;
if (networkDisplayName === networkName) {
return true
}
});
mappedSubnets = filteredSubnets.map((o) => {
const network = networkContent.findBy('selfLink', o.network);
const networkDisplayName = network.name;
return {
label: `${ o.name }(${ o.ipCidrRange })`,
value: o.name,
secondaryIpRanges: o.secondaryIpRanges,
networkDisplayName
}
});
const defaultSubnetAry = [{
label: this.intl.t('clusterNew.googlegke.ipPolicyCreateSubnetwork.autoLabel'),
value: '',
}];
out = [...defaultSubnetAry, ...mappedSubnets];
}
return out;
}),
selectedCloudCredential: computed('config.googleCredentialSecret', function() {
const {
model: { cloudCredentials = [] },
config: { googleCredentialSecret }
} = this;
if (isEmpty(cloudCredentials) && isEmpty(googleCredentialSecret)) {
return null;
} else {
return cloudCredentials.findBy('id', googleCredentialSecret.includes('cattle-global-data:') ? googleCredentialSecret : `cattle-global-data:${ googleCredentialSecret }`);
}
}),
versionChoices: computed('versions.validMasterVersions.[]', 'config.kubernetesVersion', function() {
const {
versions: { validMasterVersions = [] },
config: { kubernetesVersion },
mode,
} = this;
return this.serivceVersions.parseCloudProviderVersionChoices(validMasterVersions, kubernetesVersion, mode);
}),
zoneChoices: computed('zones.[]', function() {
let out = (get(this, 'zones') || []).slice();
out.forEach((obj) => {
set(obj, 'sortName', sortableNumericSuffix(obj.name));
set(obj, 'displayName', `${ obj.name } (${ obj.description })`);
set(obj, 'disabled', obj.status.toLowerCase() !== 'up');
});
return out.sortBy('sortName')
}),
defaultConfig() {
const neu = { ...DEFAULT_GKE_CONFIG };
const neuNp = { ...DEFAULT_GKE_NODE_POOL_CONFIG };
set(neu, 'nodePools', [neuNp]);
return neu;
},
willSave() {
this.validateNodePools();
if (!isEmpty(this.errors)) {
return false;
}
const config = get(this, 'config') || {}
const locationType = get(this, 'locationType');
if ( locationType === this.google.defaultZoneType ) {
set(config, 'region', null);
} else {
set(config, 'zone', null);
}
if (get(this, 'config.useIpAliases')) {
set(config, 'clusterIpv4Cidr', null);
}
if (!get(config, 'masterAuthorizedNetworks.enabled')) {
delete config.masterAuthorizedNetworks.cidrBlocks
}
const locationContent = get(this, 'locationContent')
const locations = locationContent.filter((l) => l.checked).map((l) => l.name)
if (this.locationType === 'zonal') {
if (locations.length > 0) {
locations.push(get(config, 'zone'))
set(config, 'locations', locations)
}
}
return this._super(...arguments);
},
validateNodePools() {
const nodePools = get(this, 'primaryResource.gkeConfig.nodePools');
const errors = [];
if (!isEmpty(nodePools)) {
const nodePoolErrors = [];
nodePools.forEach((np) => {
const npErr = np.validationErrors();
nodePoolErrors.push(npErr)
});
if (!isEmpty(nodePoolErrors)) {
errors.pushObjects(nodePoolErrors.flat());
}
}
set(this, 'errors', errors);
return errors.length >= 1 ? true : null;
},
});

View File

@ -0,0 +1,556 @@
<AccordionList @showExpandAll="false" as |al expandFn|>
{{#if (and (eq step 5) importedClusterIsPending)}}
<ImportCommand
@cluster={{primaryResource}}
@showClusterAdminWarning={{false}}
@showGkeksClusterWarning={{true}}
/>
<div class="footer-actions">
<button class="btn bg-primary" type="button" {{action "close"}}>
{{t "clusterNew.rke.done"}}
</button>
</div>
{{/if}}
{{#if (gte step 1)}}
<AccordionListItem
@title={{t "clusterNew.googlegke.access.title"}}
@detail={{t "clusterNew.googlegke.access.detail"}}
@expandAll={{expandAll}}
@expand={{action expandFn}}
@expandOnInit={{true}}
>
<div class="row">
<div class="col span-3">
<label class="acc-label">
{{t "clusterNew.googlegke.access.projectId.label"}}
<FieldRequired />
</label>
<InputOrDisplay
@editable={{and isNew (eq step 1)}}
@value={{config.projectID}}
>
{{input type="text" value=config.projectID classNames="form-control"
}}
</InputOrDisplay>
</div>
{{#if (eq step 1)}}
<FormAuthCloudCredential
@cloudCredentialKey="gkeConfig.googleCredentialSecret"
@mode={{mode}}
@cancel={{action "close"}}
@cloudCredentials={{cloudCredentials}}
@createLabel="clusterNew.googlegke.access.next"
@driverName="google"
@parseAndCollectErrors={{action "errorHandler"}}
@finishAndSelectCloudCredential={{action
"finishAndSelectCloudCredential"
}}
@primaryResource={{primaryResource}}
@progressStep={{action "loadZones"}}
@disableSave={{not config.projectID}}
/>
{{else}}
<div class="row">
<div class="col span-4">
<label class="acc-label">
{{t "clusterNew.googlegke.access.cloudCred"}}
</label>
<div>
{{selectedCloudCredential.displayName}}
</div>
</div>
</div>
{{/if}}
</div>
</AccordionListItem>
<TopErrors @errors={{allErrors}} />
{{/if}}
{{#if (gte step 2)}}
<AccordionListItem
@title={{t "clusterNew.googlegke.clusterLocation.title"}}
@detail={{t "clusterNew.googlegke.clusterLocation.detail"}}
@expandAll={{expandAll}}
@expand={{action expandFn}}
@expandOnInit={{true}}
>
<div class="row">
<div class="col span-6">
<label class="acc-label">
{{t "clusterNew.googlegke.locationType.label"}}
</label>
<InputOrDisplay
@editable={{and isNew (eq step 2)}}
@value={{locationType}}
>
<div class="radio">
<label>
{{radio-button selection=locationType value="zonal"}}
{{t "clusterNew.googlegke.locationType.zone"}}
</label>
</div>
<div class="radio">
<label>
{{radio-button selection=locationType value="regional"}}
{{t "clusterNew.googlegke.locationType.region"}}
</label>
</div>
</InputOrDisplay>
</div>
{{#if (eq locationType "zonal")}}
<div class="col span-4">
<label class="acc-label">
{{t "clusterNew.googlegke.zone.label"}}
</label>
<InputOrDisplay
@editable={{and isNew (eq step 2)}}
@value={{config.zone}}
>
<NewSelect
@classNames="form-control select-algin-checkbox"
@content={{zoneChoices}}
@disabled={{editing}}
@localizedPrompt={{true}}
@optionLabelPath="name"
@optionValuePath="name"
@prompt="clusterNew.googlegke.zone.prompt"
@value={{config.zone}}
/>
</InputOrDisplay>
</div>
<div class="col span-2">
<label class="acc-label">
{{t "clusterNew.googlegke.locations.label"}}
</label>
{{#each locationContent as |location|}}
<div class="checkbox">
<label>
{{input type="checkbox" checked=location.checked}}
{{location.name}}
</label>
</div>
{{/each}}
</div>
{{/if}}
{{#if (eq locationType "regional")}}
<div class="col span-6">
<label class="acc-label">
{{t "clusterNew.googlegke.region.label"}}
</label>
<InputOrDisplay
@editable={{and isNew (eq step 2)}}
@value={{config.region}}
>
<NewSelect
@classNames="form-control select-algin-checkbox"
@content={{regionChoices}}
@disabled={{editing}}
@localizedPrompt={{true}}
@optionLabelPath="name"
@optionValuePath="name"
@prompt="clusterNew.googlegke.region.prompt"
@value={{mut config.region}}
/>
</InputOrDisplay>
</div>
{{/if}}
</div>
{{#if (eq step 2)}}
<SaveCancel
@createLabel="clusterNew.googlegke.clusterLocation.createLabel"
@savingLabel="clusterNew.googlegke.clusterLocation.savingLabel"
@save={{action "checkServiceAccount"}}
@cancel={{close}}
/>
{{/if}}
</AccordionListItem>
{{/if}}
{{#if (gte step 3)}}
<AccordionListItem
@title={{t "clusterNew.googlegke.clusterOption.title"}}
@detail={{t "clusterNew.googlegke.clusterOption.detail"}}
@expandAll={{expandAll}}
@expand={{action expandFn}}
@expandOnInit={{true}}
>
<div class="row">
<div class="col span-6">
<label class="acc-label">
{{t "clusterNew.googlegke.masterVersion.label"}}
</label>
<NewSelect
@classNames="form-control"
@content={{versionChoices}}
@value={{config.kubernetesVersion}}
/>
</div>
<div class="col span-6">
<label class="acc-label">
{{t "clusterNew.googlegke.clusterIpv4Cidr.label"}}
</label>
<InputOrDisplay @editable={{isNew}} @value={{config.clusterIpv4Cidr}}>
<InputCidr @value={{mut config.clusterIpv4Cidr}} />
</InputOrDisplay>
</div>
</div>
<section class="networks">
<div class="row">
<div class="col span-6">
<label class="acc-label">
{{t "clusterNew.googlegke.network.label"}}
</label>
<InputOrDisplay @editable={{isNew}} @value={{config.network}}>
{{searchable-select
content=networkContent
classNames="form-control"
value=config.network
optionValuePath="name"
optionLabelPath="name"
}}
</InputOrDisplay>
</div>
<div class="col span-6">
<label class="acc-label">
{{t
(if
config.ipAllocationPolicy.useIpAliases
"clusterNew.googlegke.nodeSubNetwork.label"
"clusterNew.googlegke.subNetwork.label"
)
}}
</label>
<InputOrDisplay @editable={{isNew}} @value={{config.subnetwork}}>
{{searchable-select
content=subNetworkContent
classNames="form-control"
value=config.subnetwork
readOnly=(lte subNetworkContent.length 0)
}}
</InputOrDisplay>
</div>
</div>
</section>
<section class="ip-allocation-policies mb-20">
<div class="row">
<div class="col span-6">
<div class="checkbox">
<label>
{{input
type="checkbox"
checked=config.ipAllocationPolicy.useIpAliases
disabled=(or
config.privateClusterConfig.enablePrivateNodes editing
)
}}
{{t "clusterNew.googlegke.useIpAliases.label"}}
</label>
</div>
<div class="checkbox">
<label>
{{input
type="checkbox"
checked=config.networkPolicyEnabled
disabled=editing
}}
{{t "clusterNew.googlegke.networkPolicy.label"}}
</label>
</div>
</div>
</div>
<div class="row">
{{#if config.ipAllocationPolicy.createSubnetwork}}
<div class="col span-6">
<label class="acc-label">
{{t "clusterNew.googlegke.ipPolicySubnetworkName.label"}}
<FieldRequired />
</label>
<InputOrDisplay
@editable={{isNew}}
@value={{config.ipAllocationPolicy.subnetworkName}}
>
{{input
value=config.ipAllocationPolicy.subnetworkName
placeholder=(t
"clusterNew.googlegke.ipPolicySubnetworkName.placeholder"
)
disabled=(not config.ipAllocationPolicy.createSubnetwork)
}}
</InputOrDisplay>
</div>
{{else}}
<div class="col span-6">
<label class="acc-label">
{{t
"clusterNew.googlegke.ipPolicyClusterSecondaryRangeName.label"
}}
</label>
<InputOrDisplay
@editable={{isNew}}
@value={{config.ipAllocationPolicy.clusterSecondaryRangeName}}
>
{{searchable-select
content=secondaryIpRangeContent
classNames="form-control"
value=config.ipAllocationPolicy.clusterSecondaryRangeName
readOnly=disableSecondaryRangeNames
placeholder=(t
"clusterNew.googlegke.ipPolicyClusterSecondaryRangeName.placeholder"
)
}}
</InputOrDisplay>
</div>
{{/if}}
<div class="col span-6">
<label class="acc-label">
{{t
"clusterNew.googlegke.ipAllocationPolicy.clusterIpv4CidrBlock.accesslabel"
}}
</label>
<InputOrDisplay
@editable={{isNew}}
@value={{config.ipAllocationPolicy.clusterIpv4CidrBlock}}
>
<InputCidr
@value={{mut config.ipAllocationPolicy.clusterIpv4CidrBlock}}
@disabled={{not config.ipAllocationPolicy.useIpAliases}}
@placeholder={{t
"clusterNew.googlegke.ipAllocationPolicy.clusterIpv4CidrBlock.placeholder"
}}
/>
</InputOrDisplay>
</div>
</div>
<div class="row">
{{#if config.ipAllocationPolicy.createSubnetwork}}
<div class="col span-6">
<label class="acc-label">
{{t "clusterNew.googlegke.ipPolicyNodeIpv4CidrBlock.label"}}
</label>
<InputOrDisplay
@editable={{isNew}}
@value={{config.ipAllocationPolicy.nodeIpv4CidrBlock}}
>
<InputCidr
@value={{mut config.ipAllocationPolicy.nodeIpv4CidrBlock}}
@disabled={{not config.ipAllocationPolicy.createSubnetwork}}
@placeholder={{t
"clusterNew.googlegke.ipPolicyNodeIpv4CidrBlock.placeholder"
}}
/>
</InputOrDisplay>
</div>
{{else}}
<div class="col span-6">
<label class="acc-label">
{{t
"clusterNew.googlegke.ipPolicyServicesSecondaryRangeName.label"
}}
</label>
<InputOrDisplay
@editable={{isNew}}
@value={{config.ipAllocationPolicy.servicesSecondaryRangeName}}
>
{{searchable-select
content=secondaryIpRangeContent
classNames="form-control"
value=config.ipAllocationPolicy.servicesSecondaryRangeName
readOnly=disableSecondaryRangeNames
placeholder=(t
"clusterNew.googlegke.ipPolicyServicesSecondaryRangeName.placeholder"
)
}}
</InputOrDisplay>
</div>
{{/if}}
<div class="col span-6">
<label class="acc-label">
{{t "clusterNew.googlegke.ipPolicyServicesIpv4CidrBlock.label"}}
</label>
<InputOrDisplay
@editable={{isNew}}
@value={{config.ipAllocationPolicy.servicesIpv4CidrBlock}}
>
<InputCidr
@value={{mut config.ipAllocationPolicy.servicesIpv4CidrBlock}}
@disabled={{not config.ipAllocationPolicy.useIpAliases}}
@placeholder={{t
"clusterNew.googlegke.ipPolicyServicesIpv4CidrBlock.placeholder"
}}
/>
</InputOrDisplay>
</div>
</div>
</section>
<AdvancedSection @advanced={{clusterAdvanced}}>
<CruPrivateCluster
@config={{mut config.privateClusterConfig}}
@editing={{editing}}
@mode={{mode}}
@isNew={{isNew}}
/>
<CruMasterAuthNetwork
@config={{mut config.masterAuthorizedNetworks}}
@isNew={{isNew}}
@editing={{editing}}
@addMSAN={{action "addMSAN"}}
@removeMSAN={{action "removeMSAN"}}
/>
</AdvancedSection>
</AccordionListItem>
<AccordionListItem
@title={{t "clusterNew.googlegke.clusterExtras.title"}}
@detail={{t "clusterNew.googlegke.clusterExtras.detail"}}
@expandAll={{expandAll}}
@expand={{action expandFn}}
@expandOnInit={{true}}
>
<section class="cluster-addons">
<div class="row">
<div class="col span-6">
<label class="acc-label">
{{t "clusterNew.googlegke.clusterAddons.label"}}
</label>
<div class="checkbox">
<label>
{{input
type="checkbox"
checked=config.clusterAddons.horizontalPodAutoscaling
}}
{{t
"clusterNew.googlegke.clusterAddons.horizontalPodAutoscaling"
}}
</label>
</div>
<div class="checkbox">
<label>
{{input
type="checkbox"
checked=config.clusterAddons.httpLoadBalancing
}}
{{t "clusterNew.googlegke.clusterAddons.httpLoadBalancing"}}
</label>
</div>
<div class="checkbox">
<label>
{{input
type="checkbox"
checked=config.clusterAddons.networkPolicyConfig
}}
{{t "clusterNew.googlegke.clusterAddons.networkPolicyConfig"}}
</label>
</div>
</div>
<div class="col span-6">
<label class="acc-label">
{{t "clusterNew.googlegke.clusterFeatures.label"}}
</label>
<div class="checkbox">
<label class={{if editing "text-muted"}}>
{{input
type="checkbox"
checked=config.enableKubernetesAlpha
disabled=editing
}}
{{t "clusterNew.googlegke.alphaFeatures.label"}}
</label>
</div>
{{#if config.enableKubernetesAlpha}}
<BannerMessage
@icon="icon-alert"
@color="bg-warning mb-10"
@message={{t
"clusterNew.googlegke.alphaFeatures.warning"
htmlSafe=true
}}
/>
{{/if}}
</div>
</div>
<div class="row">
<div class="col span-6">
<label class="acc-label">
{{t "clusterNew.googlegke.loggingService.label"}}
</label>
<NewSelect
@classNames="form-control select-algin-checkbox"
@content={{loggingServiceChoices}}
@value={{config.loggingService}}
/>
</div>
<div class="col span-6">
<label class="acc-label">
{{t "clusterNew.googlegke.monitoringService.label"}}
</label>
<NewSelect
@classNames="form-control select-algin-checkbox"
@content={{monitoringServiceChoices}}
@value={{config.monitoringService}}
/>
</div>
</div>
<div class="row">
<div class="col span-6">
<label class="acc-label">
{{t "clusterNew.googlegke.maintenanceWindow.label"}}
</label>
<div class="form-control-static">
<NewSelect
@classNames="form-control"
@content={{maintenanceWindowTimes}}
@value={{mut config.maintenanceWindow}}
/>
</div>
</div>
</div>
</section>
</AccordionListItem>
<AccordionListItem
@title={{t "clusterNew.googlegke.nodePool.title"}}
@detail={{t "clusterNew.googlegke.nodePool.detail"}}
@expandAll={{expandAll}}
@expand={{action expandFn}}
@expandOnInit={{true}}
>
<div class="row">
{{#each config.nodePools as |nodePool|}}
<GkeNodePoolRow
@originalCluster={{model.originalCluster}}
@isNew={{isNew}}
@machineTypes={{machineTypes}}
@mode={{mode}}
@model={{nodePool}}
@removeNodePool={{action "removeNodePool"}}
@nodeVersions={{versions.validNodeVersions}}
@controlPlaneVersion={{config.kubernetesVersion}}
@cluster={{cluster}}
/>
{{else}}
<div class="p-20">
{{t "clusterNew.googlegke.nodePool.noNodes"}}
</div>
{{/each}}
</div>
<div class="row mt-15">
<button
class="btn bg-link icon-btn pull-left"
type="button"
{{action "addNodePool"}}
>
<span class="darken">
<i class="icon icon-plus text-small"></i>
</span>
<span>
{{t "clusterNew.googlegke.nodePool.addLabel"}}
</span>
</button>
</div>
</AccordionListItem>
<TopErrors @errors={{allErrors}} />
<SaveCancel
@editing={{eq mode "edit"}}
@save={{action "driverSave"}}
@cancel={{close}}
@saving={{saving}}
@saved={{saved}}
/>
{{/if}}
</AccordionList>

View File

@ -1,207 +1,39 @@
import Component from '@ember/component'
import ClusterDriver from 'shared/mixins/cluster-driver';
import layout from './template';
import Component from '@ember/component';
import {
get, set, computed, observer, setProperties
computed, get, observer, set, setProperties
} from '@ember/object';
import { sortableNumericSuffix } from 'shared/utils/util';
import { reject, all } from 'rsvp';
import { inject as service } from '@ember/service';
import { equal } from '@ember/object/computed';
import $ from 'jquery';
import { inject as service } from '@ember/service';
import { isEmpty } from '@ember/utils';
const times = [
{
value: null,
label: 'Any Time',
},
{
value: '00:00',
label: '12:00AM',
},
{
value: '03:00',
label: '3:00AM',
},
{
value: '06:00',
label: '6:00AM',
},
{
value: '09:00',
label: '9:00AM',
},
{
value: '12:00',
label: '12:00PM',
},
{
value: '15:00',
label: '3:00PM',
},
{
value: '19:00',
label: '7:00PM',
},
{
value: '21:00',
label: '9:00PM',
},
]
const imageType = [
{
label: 'clusterNew.googlegke.imageType.UBUNTU',
value: 'UBUNTU',
},
{
label: 'clusterNew.googlegke.imageType.COS',
value: 'COS'
},
]
const diskType = [
{
label: 'clusterNew.googlegke.diskType.pd-standard',
value: 'pd-standard',
},
{
label: 'clusterNew.googlegke.diskType.pd-ssd',
value: 'pd-ssd',
}
]
const REGIONS = ['asia-east1', 'asia-east2', 'asia-northeast1', 'asia-northeast2', 'asia-south1', 'asia-southeast1', 'australia-southeast1', 'europe-north1', 'europe-west1', 'europe-west2', 'europe-west3', 'europe-west4', 'europe-west6', 'northamerica-northeast1', 'southamerica-east1', 'us-central1', 'us-east1', 'us-east4', 'us-west1', 'us-west2']
const DEFAULT_AUTH_SCOPES = ['devstorage.read_only', 'logging.write', 'monitoring', 'servicecontrol', 'service.management.readonly', 'trace.append']
const ZONE_TYPE = 'zonal';
const REGION_TYPE = 'regional';
const OAUTH_SCOPE_OPTIONS = {
DEFAULT: 'default',
FULL: 'full',
CUSTOM: 'custom'
};
const GOOGLE_AUTH_URL_PREFIX = 'https://www.googleapis.com/auth/';
const GOOGLE_AUTH_DEFAULT_URLS = DEFAULT_AUTH_SCOPES.map((a) => `${ GOOGLE_AUTH_URL_PREFIX }${ a }`);
const GOOGLE_FULL_AUTH_URL = 'https://www.googleapis.com/auth/cloud-platform';
function getValueFromOauthScopes(oauthScopes, key, defaultValue) {
const filteredValues = oauthScopes
.filter((scope) => scope.indexOf(key) !== -1)
.map((scope) => {
return scope
.replace(GOOGLE_AUTH_URL_PREFIX, '')
.replace(key, '').split('.')
})
.filter((splitScopes) => splitScopes.length <= 2);
if (filteredValues.length !== 1) {
return defaultValue || 'none';
}
return filteredValues[0].length === 1
? key
: `${ key }.${ filteredValues[0][1] }`;
}
/**
* This oauthScopesMapper is responsible for both the mapping to oauthScopes
* and unmapping from oauthscopes to form values. If you modify either
* method ensure that the other reflects your changes.
*/
const oauthScopesMapper = {
mapOauthScopes(oauthScopesSelection, scopeConfig) {
if (oauthScopesSelection === OAUTH_SCOPE_OPTIONS.DEFAULT) {
return GOOGLE_AUTH_DEFAULT_URLS;
} else if (oauthScopesSelection === OAUTH_SCOPE_OPTIONS.FULL) {
return [GOOGLE_FULL_AUTH_URL];
} else if (oauthScopesSelection === OAUTH_SCOPE_OPTIONS.CUSTOM) {
scopeConfig = scopeConfig || {};
let arr = [];
Object.keys(scopeConfig).map((key) => {
if (scopeConfig[key] !== 'none') {
arr.pushObject(`https://www.googleapis.com/auth/${ scopeConfig[key] }`)
}
})
return arr;
}
},
unmapOauthScopes(oauthScopes) {
const containsUrls = oauthScopes && oauthScopes.length > 0;
if (!containsUrls) {
return { oauthScopesSelection: OAUTH_SCOPE_OPTIONS.DEFAULT };
}
const isAllAndOnlyDefaultUrls = ( GOOGLE_AUTH_DEFAULT_URLS.length === oauthScopes.length
&& GOOGLE_AUTH_DEFAULT_URLS.every((url) => oauthScopes.indexOf(url) !== -1) );
if (isAllAndOnlyDefaultUrls) {
return { oauthScopesSelection: OAUTH_SCOPE_OPTIONS.DEFAULT }
}
const isOnlyTheFullUrl = oauthScopes.length === 1
&& oauthScopes[0] === GOOGLE_FULL_AUTH_URL;
if (isOnlyTheFullUrl) {
return { oauthScopesSelection: OAUTH_SCOPE_OPTIONS.FULL }
}
return {
oauthScopesSelection: OAUTH_SCOPE_OPTIONS.CUSTOM,
scopeConfig: {
userInfo: getValueFromOauthScopes(oauthScopes, 'userinfo', 'none'),
computeEngine: getValueFromOauthScopes(oauthScopes, 'compute', 'none'),
storage: getValueFromOauthScopes(oauthScopes, 'devstorage', 'devstorage.read_only'),
taskQueue: getValueFromOauthScopes(oauthScopes, 'taskqueue', 'none'),
bigQuery: getValueFromOauthScopes(oauthScopes, 'bigquery', 'none'),
cloudSQL: getValueFromOauthScopes(oauthScopes, 'sqlservice', 'none'),
cloudDatastore: getValueFromOauthScopes(oauthScopes, 'clouddatastore', 'none'),
stackdriverLoggingAPI: getValueFromOauthScopes(oauthScopes, 'logging', 'logging.write'),
stackdriverMonitoringAPI: getValueFromOauthScopes(oauthScopes, 'monitoring', 'monitoring'),
cloudPlatform: getValueFromOauthScopes(oauthScopes, 'cloud-platform', 'none'),
bigtableData: getValueFromOauthScopes(oauthScopes, 'bigtable.data', 'none'),
bigtableAdmin: getValueFromOauthScopes(oauthScopes, 'bigtable.admin', 'none'),
cloudPub: getValueFromOauthScopes(oauthScopes, 'pubsub', 'none'),
serviceControl: getValueFromOauthScopes(oauthScopes, 'servicecontrol', 'none'),
serviceManagement: getValueFromOauthScopes(oauthScopes, 'service.management', 'service.management.readonly'),
stackdriverTrace: getValueFromOauthScopes(oauthScopes, 'trace', 'trace.append'),
cloudSourceRepositories: getValueFromOauthScopes(oauthScopes, 'source', 'none'),
cloudDebugger: getValueFromOauthScopes(oauthScopes, 'cloud_debugger', 'none'),
}
};
}
};
import $ from 'jquery';
import { all, reject } from 'rsvp';
import ClusterDriver from 'shared/mixins/cluster-driver';
import { sortableNumericSuffix } from 'shared/utils/util';
import layout from './template';
export default Component.extend(ClusterDriver, {
intl: service(),
settings: service(),
versionChoiceService: service('version-choices'),
google: service(),
layout,
configField: 'googleKubernetesEngineConfig',
step: 1,
zones: null,
versions: null,
machineTypes: null,
initialMasterVersion: null,
maintenanceWindowTimes: times,
eipIdContent: [],
imageTypeContent: imageType,
clusterAdvanced: false,
nodeAdvanced: false,
diskTypeContent: diskType,
scopeConfig: {},
diskTypeContent: null,
eipIdContent: null,
hideNewField: false,
locationType: ZONE_TYPE,
imageTypeContent: null,
initialMasterVersion: null,
locationType: null,
machineTypes: null,
maintenanceWindowTimes: null,
nodeAdvanced: false,
scopeConfig: null,
versions: null,
zones: null,
isNew: equal('mode', 'new'),
editing: equal('mode', 'edit'),
@ -209,6 +41,16 @@ export default Component.extend(ClusterDriver, {
init() {
this._super(...arguments);
// defaults
setProperties(this, {
maintenanceWindowTimes: this.google.maintenanceWindows,
imageTypeContent: this.google.imageTypes,
diskTypeContent: this.google.diskTypes,
locationType: this.google.defaultZoneType,
eipIdContent: [],
scopeConfig: {},
});
let config = get(this, 'cluster.googleKubernetesEngineConfig');
if ( !config ) {
@ -232,7 +74,7 @@ export default Component.extend(ClusterDriver, {
setProperties(this, {
'cluster.googleKubernetesEngineConfig': config,
oauthScopesSelection: OAUTH_SCOPE_OPTIONS.DEFAULT,
oauthScopesSelection: this.google.oauthScopeOptions.DEFAULT,
scopeConfig: {
userInfo: 'none',
computeEngine: 'none',
@ -305,7 +147,7 @@ export default Component.extend(ClusterDriver, {
if (!get(this, 'oauthScopesSelection')) {
const oauthScopes = get(config, 'oauthScopes')
const { oauthScopesSelection, scopeConfig } = oauthScopesMapper.unmapOauthScopes(oauthScopes);
const { oauthScopesSelection, scopeConfig } = this.google.unmapOauthScopes(oauthScopes);
set(this, 'oauthScopesSelection', oauthScopesSelection);
if (scopeConfig) {
@ -316,17 +158,17 @@ export default Component.extend(ClusterDriver, {
setProperties(this, {
initialMasterVersion: get(this, 'config.masterVersion'),
regionChoices: REGIONS.map((region) => {
regionChoices: this.google.regions.map((region) => {
return { name: region }
}),
locationType: get(this, 'config.zone') ? ZONE_TYPE : REGION_TYPE,
locationType: get(this, 'config.zone') ? this.google.defaultZoneType : this.google.defaultRegionType,
})
},
actions: {
clickNext() {
if (isEmpty(get(this, 'config.projectId'))) {
this.parseProjectId();
set(this, 'config.projectId', this.google.parseProjectId(get(this, 'config')));
}
$('BUTTON[type="submit"]').click();
},
@ -383,7 +225,8 @@ export default Component.extend(ClusterDriver, {
return;
}
this.parseProjectId();
set(this, 'config.projectId', this.google.parseProjectId(get(this, 'config')));
}),
zoneChanged: observer('config.zone', 'zones.[]', function() {
@ -789,7 +632,7 @@ export default Component.extend(ClusterDriver, {
data: {
credentials: get(this, 'config.credential'),
projectId: get(this, 'config.projectId'),
region: locationType === ZONE_TYPE ? `${ zone.split('-')[0] }-${ zone.split('-')[1] }` : get(this, 'config.region'),
region: locationType === this.google.defaultZoneType ? `${ zone.split('-')[0] }-${ zone.split('-')[1] }` : get(this, 'config.region'),
}
}).then((xhr) => {
const out = xhr.body.items || [];
@ -897,7 +740,7 @@ export default Component.extend(ClusterDriver, {
const locationType = get(this, 'locationType');
if ( locationType === ZONE_TYPE ) {
if ( locationType === this.google.defaultZoneType ) {
set(config, 'region', null);
} else {
set(config, 'zone', null);
@ -935,7 +778,7 @@ export default Component.extend(ClusterDriver, {
const oauthScopesSelection = get(this, 'oauthScopesSelection');
const scopeConfig = get(this, 'scopeConfig');
set(config, 'oauthScopes', oauthScopesMapper.mapOauthScopes(oauthScopesSelection, scopeConfig));
set(config, 'oauthScopes', this.google.mapOauthScopes(oauthScopesSelection, scopeConfig));
const taints = get(this, 'taints') || []
@ -952,18 +795,4 @@ export default Component.extend(ClusterDriver, {
return this._super(...arguments);
},
parseProjectId() {
const str = get(this, 'config.credential');
if ( str ) {
try {
const obj = JSON.parse(str);
// Note: this is a Google project id, not ours.
const projectId = obj.project_id;
set(this, 'config.projectId', projectId);
} catch (e) {
}
}
},
});

View File

@ -367,7 +367,7 @@
{{#if (gt subNetworkContent.length 0)}}
<div class="col span-6">
<label class="acc-label">
{{t "clusterNew.googlegke.subNetwork.label"}}
{{t "clusterNew.googlegke.nodeSubNetwork.label"}}
</label>
{{#input-or-display
editable=isNew

View File

@ -0,0 +1,252 @@
import Component from '@ember/component';
import {
computed, get, observer, set, setProperties
} from '@ember/object';
import { alias, equal, union } from '@ember/object/computed';
import { next } from '@ember/runloop';
import { inject as service } from '@ember/service';
import { isEmpty } from '@ember/utils';
import { all } from 'rsvp';
import ClusterDriver from 'shared/mixins/cluster-driver';
import { sortableNumericSuffix } from 'shared/utils/util';
import layout from './template';
export default Component.extend(ClusterDriver, {
globalStore: service(),
growl: service(),
settings: service(),
intl: service(),
google: service(),
layout,
configField: 'gkeConfig',
step: 1,
loading: false,
nodeForInfo: null,
loadingClusters: false,
loadFailedAllClusters: false,
regionChoices: null,
errors: null,
otherErrors: null,
clusterErrors: null,
selectedCred: null,
isPostSave: false,
config: null,
zones: null,
locationType: null,
isEdit: equal('mode', 'edit'),
clusterState: alias('model.originalCluster.state'),
allErrors: union('errors', 'otherErrors', 'clusterErrors'),
init() {
this._super(...arguments);
setProperties(this, {
errors: [],
clusterErrors: [],
otherErrors: [],
zones: [],
locationType: this.google.defaultRegionType,
regionChoices: this.google.regions.map((region) => ({ name: region })),
});
if (this.isEdit) {
const cloudCredId = get(this, 'model.cluster.gkeConfig.googleCredentialSecret');
const cloudCred = (this.model.cloudCredentials || []).find((cc) => cc.id === cloudCredId);
if (!isEmpty(cloudCred)) {
next(() => {
this.send('finishAndSelectCloudCredential', cloudCred);
});
}
} else {
this.bootstrapGkeV2Cluster();
}
},
actions: {
clickNext() {},
finishAndSelectCloudCredential(cred) {
if (isEmpty(cred)) {
set(this, 'config.googleCredentialSecret', null);
set(this, 'selectedCred', null);
} else {
set(this, 'config.googleCredentialSecret', cred.id);
set(this, 'selectedCred', cred);
this.send('checkServiceAccount');
}
},
checkServiceAccount(cb) {
set(this, 'errors', []);
const config = get(this, `cluster.${ this.configField }`);
return all([
this.google.fetchZones(config, this.saved),
]).then((resp) => {
const [zones] = resp;
setProperties(this, {
step: 2,
zones,
});
if (cb) {
cb(true)
}
}).catch((err) => {
this.send('errorHandler', err);
if (cb) {
cb(false)
}
});
},
async loadClusters(cb) {
const errors = [];
let step = 3;
let allClusters;
set(this, 'loadingClusters', true);
try {
const config = get(this, `cluster.${ this.configField }`);
allClusters = await this.google.fetchClusters(config, this.saved ?? false);
setProperties(this, {
allClusters: (allClusters || []).map((c) => {
return {
label: c.name,
value: c.name
};
}),
step,
});
setProperties(this, {
loadingClusters: false,
step,
});
if (cb) {
cb()
}
} catch (err) {
errors.pushObject(`Failed to load Clusters from GKE: ${ err.message }`);
// EKS List Clusters API fails sometimes to list this, user cnn input a cluster name though so dont fail
setProperties(this, {
loadFailedAllClusters: true,
errors
});
if (cb) {
cb(false, err);
}
} finally {
setProperties(this, {
loadingClusters: false,
step,
});
}
},
},
locationOrZoneChanged: observer('locationType', 'config.{region,zone}', function() {
const { config, locationType } = this;
if (locationType === 'zonal') {
if (!isEmpty(get(config, 'region'))) {
delete this.config.region;
}
set(this, 'config.zone', 'us-west2-a');
if (!isEmpty(config?.zone)) {
this.send('loadClusters');
}
} else {
if (!isEmpty(get(config, 'zone'))) {
delete this.config.zone;
}
set(this, 'config.region', 'us-west2');
if (!isEmpty(config?.region)) {
this.send('loadClusters');
}
}
}),
zoneChoices: computed('zones.[]', function() {
let out = (get(this, 'zones') || []).slice();
out.forEach((obj) => {
setProperties(obj, {
sortName: sortableNumericSuffix(obj.name),
displayName: `${ obj.name } (${ obj.description })`,
disabled: obj.status.toLowerCase() !== 'up',
});
});
return out.sortBy('sortName')
}),
disableImport: computed('step', 'config.{googleCredentialSecret,clusterName}', function() {
const { step, config: { googleCredentialSecret, clusterName } } = this;
if (step <= 3 && !isEmpty(googleCredentialSecret) && !isEmpty(clusterName)) {
return false;
}
return true;
}),
cloudCredentials: computed('model.cloudCredentials', function() {
const { model: { cloudCredentials } } = this;
return cloudCredentials.filter((cc) => Object.prototype.hasOwnProperty.call(cc, 'googlecredentialConfig'));
}),
doneSaving() {
const {
isPostSave,
model: {
cluster: {
gkeConfig = {},
gkeStatus = {},
}
}
} = this;
const privateEndpoint = get(gkeConfig, 'privateClusterConfig.enablePrivateEndpoint') || get(gkeStatus, 'upstreamSpec.privateClusterConfig.enablePrivateEndpoint') || false;
if (isPostSave && privateEndpoint) {
set(this, 'step', 4);
return;
}
if (this.close) {
this.close();
}
},
bootstrapGkeV2Cluster() {
const gkeConfig = this.globalStore.createRecord({
clusterName: '',
imported: true,
region: 'us-west2',
type: 'gkeclusterconfigspec',
});
set(this, 'model.cluster.gkeConfig', gkeConfig);
set(this, 'config', gkeConfig);
},
});

View File

@ -0,0 +1,185 @@
<AccordionList @showExpandAll="false" as |al expandFn|>
{{#if (gte step 1)}}
<AccordionListItem
@title={{t "clusterNew.googlegke.access.title"}}
@detail={{t "clusterNew.googlegke.access.detail"}}
@expandAll={{expandAll}}
@expand={{action expandFn}}
@expandOnInit={{true}}
>
<div class="row">
<div class="col span-3">
<label class="acc-label">
{{t "clusterNew.googlegke.access.projectId.label"}}<FieldRequired />
</label>
{{#if (eq step 1)}}
{{input type="text" value=config.projectID classNames="form-control"
}}
{{else}}
<div>
{{config.projectID}}
</div>
{{/if}}
</div>
{{#if (eq step 1)}}
<FormAuthCloudCredential
@cloudCredentialKey="gkeConfig.googleCredentialSecret"
@mode={{mode}}
@cancel={{action "close"}}
@cloudCredentials={{cloudCredentials}}
@createLabel="clusterNew.googlegke.access.next"
@driverName="google"
@parseAndCollectErrors={{action "errorHandler"}}
@finishAndSelectCloudCredential={{action
"finishAndSelectCloudCredential"
}}
@primaryResource={{primaryResource}}
@progressStep={{action "checkServiceAccount"}}
@disableSave={{not config.projectID}}
/>
{{else}}
<div class="row">
<div class="col span-4">
<label class="acc-label">
{{t "clusterNew.googlegke.access.cloudCred"}}
</label>
<div>
{{config.googleCredentialSecret}}
</div>
</div>
</div>
{{/if}}
</div>
</AccordionListItem>
{{/if}}
{{#if (gte step 2)}}
<AccordionListItem
@title={{t "clusterNew.googlegke.import.locations.title"}}
@detail={{t "clusterNew.googlegke.import.locations.detail"}}
@expandAll={{expandAll}}
@expand={{action expandFn}}
@expandOnInit={{true}}
>
<div class="row">
<div class="col span-6">
<label class="acc-label">
{{t "clusterNew.googlegke.locationType.label"}}
</label>
<div class="radio">
<label>
{{radio-button selection=locationType value="zonal"}}
{{t "clusterNew.googlegke.locationType.zone"}}
</label>
</div>
<div class="radio">
<label>
{{radio-button selection=locationType value="regional"}}
{{t "clusterNew.googlegke.locationType.region"}}
</label>
</div>
</div>
{{#if (eq locationType "zonal")}}
<div class="col span-4">
<label class="acc-label">
{{t "clusterNew.googlegke.zone.label"}}
</label>
<NewSelect
@classNames="form-control select-algin-checkbox"
@content={{zoneChoices}}
@disabled={{editing}}
@localizedPrompt={{true}}
@optionLabelPath="name"
@optionValuePath="name"
@prompt="clusterNew.googlegke.zone.prompt"
@value={{config.zone}}
/>
</div>
{{/if}}
{{#if (eq locationType "regional")}}
<div class="col span-6">
<label class="acc-label">
{{t "clusterNew.googlegke.region.label"}}
</label>
<NewSelect
@classNames="form-control select-algin-checkbox"
@content={{regionChoices}}
@disabled={{editing}}
@localizedPrompt={{true}}
@optionLabelPath="name"
@optionValuePath="name"
@prompt="clusterNew.googlegke.region.prompt"
@value={{mut config.region}}
/>
</div>
{{/if}}
</div>
{{#if (eq step 2)}}
<SaveCancel
@createLabel="clusterNew.googlegke.import.clusterSelect.loadLabel"
@save={{action "loadClusters"}}
@cancel={{action "close"}}
/>
{{/if}}
</AccordionListItem>
{{/if}}
{{#if (eq step 3)}}
<AccordionListItem
@title={{t "clusterNew.googlegke.import.clusterSelect.title"}}
@detail={{t "clusterNew.googlegke.import.clusterSelect.detail"}}
@expandAll={{expandAll}}
@expand={{action expandFn}}
@expandOnInit={{true}}
>
<div class="row">
<div class="col span-6">
{{#if loadingClusters}}
<section class="horizontal-form">
<div class="text-center">
<i class="icon icon-spinner icon-spin"></i>
</div>
</section>
{{else if loadFailedAllClusters}}
<label class="acc-label" for="">
{{t "clusterNew.googlegke.import.clusterSelect.input.label"}}
{{field-required}}
</label>
<Input
@type="text"
@classNames="form-control"
@value={{config.clusterName}}
/>
{{else}}
<label class="acc-label" for="">
{{t "clusterNew.googlegke.import.clusterSelect.select.label"}}
{{field-required}}
</label>
<SearchableSelect
class="form-control"
@value={{mut config.clusterName}}
@content={{allClusters}}
@allowCustom={{true}}
/>
{{/if}}
</div>
</div>
{{#if (eq step 3)}}
<SaveCancel
@createLabel="clusterNew.googlegke.import.clusterSelect.createLabel"
@save={{action "driverSave"}}
@saveDisabled={{disableImport}}
@editing={{isEdit}}
@cancel={{action "close"}}
/>
{{/if}}
</AccordionListItem>
{{/if}}
{{#if (eq step 4)}}
<ImportCommand @cluster={{primaryResource}} />
<div class="footer-actions">
<button class="btn bg-primary" type="button" {{action "close"}}>
{{t "clusterNew.rke.done"}}
</button>
</div>
{{/if}}
<TopErrors @errors={{mut allErrors}} />
</AccordionList>

View File

@ -1,18 +1,19 @@
import Component from '@ember/component';
import {
get, set, computed, observer, setProperties
computed, get, observer, set, setProperties
} from '@ember/object';
import { inject as service } from '@ember/service';
import ViewNewEdit from 'shared/mixins/view-new-edit';
import ChildHook from 'shared/mixins/child-hook';
import { alias, equal } from '@ember/object/computed';
import { loadStylesheet, proxifyUrl } from 'shared/utils/load-script';
import layout from './template';
import { inject as service } from '@ember/service';
import { isEmpty } from '@ember/utils';
import $ from 'jquery';
import ChildHook from 'shared/mixins/child-hook';
import ViewNewEdit from 'shared/mixins/view-new-edit';
import { loadStylesheet, proxifyUrl } from 'shared/utils/load-script';
import layout from './template';
const MEMBER_CONFIG = { type: 'clusterRoleTemplateBinding', };
const BUILD_IN_UI = ['tencentkubernetesengine', 'huaweicontainercloudengine', 'oraclecontainerengine', 'linodekubernetesengine'];
const V2_DRIVERS = ['amazoneksv2', 'googlegkev2'];
export default Component.extend(ViewNewEdit, ChildHook, {
globalStore: service(),
@ -188,12 +189,20 @@ export default Component.extend(ViewNewEdit, ChildHook, {
driver: 'googlegke',
kontainerId: 'googlekubernetesengine',
},
{
displayName: 'Google GKE',
driver: 'gke',
name: 'googlegkev2',
nodePool: false,
nodeWhich: 'gke',
preSave: false,
postSave: true,
},
{
name: 'amazoneks',
driver: 'amazoneks',
kontainerId: 'amazonelasticcontainerservice',
},
// TODO - No driver entry exists for this since it is not a kontainerEngine driver, we need to disable by default the amazon eks v1 driver
{
displayName: 'Amazon EKS',
driver: 'eks',
@ -230,7 +239,7 @@ export default Component.extend(ViewNewEdit, ChildHook, {
},
];
out = out.filter( (o) => builtIn.findBy('id', o.kontainerId) || o.name === 'amazoneksv2' );
out = out.filter( (o) => builtIn.findBy('id', o.kontainerId) || V2_DRIVERS.includes(o.name));
if (custom.length > 0) {
custom.forEach( (c) => {
@ -309,6 +318,14 @@ export default Component.extend(ViewNewEdit, ChildHook, {
postSave: true,
});
out.push({
name: 'importgke',
driver: 'import-gke',
preSave: false,
postSave: true,
});
out.push({
name: 'k3s',
driver: 'import',
@ -348,6 +365,8 @@ export default Component.extend(ViewNewEdit, ChildHook, {
return null;
} else if (importProvider === 'eks') {
name = 'importeks'
} else if (importProvider === 'gke') {
name = 'importgke'
}
}
const choices = get(this, 'providerChoices');

View File

@ -0,0 +1,10 @@
import Component from '@ember/component';
import layout from './template';
export default Component.extend({
layout,
editing: false,
isNew: true,
config: null,
});

View File

@ -0,0 +1,73 @@
<div class="row">
<div class="col span-6">
<div class="form-control-static">
<div class="checkbox">
<label>
{{input type="checkbox" checked=config.enabled disabled=editing}}
{{t "clusterNew.googlegke.enableMasterAuthorizedNetwork.label"}}
</label>
</div>
</div>
</div>
{{#if config.enabled}}
<div class="col span-6">
{{#each config.cidrBlocks as |network index|}}
<div class="row">
<div class="col span-5">
{{#if (eq index 0)}}
<label class="acc-label">
{{t "generic.displayName"}}
</label>
{{/if}}
{{input
type="text"
value=network.displayName
classNames="form-control"
}}
</div>
<div class="col span-6 ml-10">
{{#if (eq index 0)}}
<label class="acc-label">
{{t
"clusterNew.googlegke.masterAuthorizedNetworkCidrBlocks.cidrBlock"
}}
</label>
{{/if}}
<div class="input-group mb-20">
<InputCidr
@classNames="form-control"
@value={{mut network.cidrBlock}}
/>
<span class="input-group-btn ml-10">
<button
class="btn bg-primary btn-sm"
type="button"
{{action removeMSAN network}}
>
<i class="icon icon-minus"></i>
<span class="sr-only">
{{t "generic.remove"}}
</span>
</button>
</span>
</div>
</div>
</div>
{{/each}}
<button
class="btn bg-link icon-btn mt-10"
type="button"
{{action addMSAN}}
>
<span class="darken">
<i class="icon icon-plus text-small"></i>
</span>
<span>
{{t
"clusterNew.googlegke.masterAuthorizedNetworkCidrBlocks.addActionLabel"
}}
</span>
</button>
</div>
{{/if}}
</div>

View File

@ -0,0 +1,47 @@
import Component from '@ember/component';
import layout from './template';
import { set } from '@ember/object';
import { next } from '@ember/runloop';
import { isEmpty } from '@ember/utils';
import { observer } from '@ember/object';
import { on } from '@ember/object/evented';
import { inject as service } from '@ember/service'
const DEFAULT_PRIVATE_CONFIG = {
enablePrivateEndpoint: false,
enablePrivateNodes: false,
masterIpv4CidrBlock: null,
};
export default Component.extend({
settings: service(),
layout,
config: null,
mode: 'new',
isNew: true,
editing: false,
defaultConfig: DEFAULT_PRIVATE_CONFIG,
enablePrivateNodesChanged: on('init', observer('config.enablePrivateNodes', function() {
const { config } = this;
const {
enablePrivateEndpoint,
masterIpv4CidrBlock,
} = this.defaultConfig;
if (this.isNew && !config?.enablePrivateNodes) {
next(this, () => {
if (config.enablePrivateNodes) {
set(this, 'config.enablePrivateEndpoint', enablePrivateEndpoint);
}
if (!isEmpty(config.masterIpv4CidrBlock)) {
set(this, 'config.masterIpv4CidrBlock', masterIpv4CidrBlock);
}
});
}
}))
});

View File

@ -0,0 +1,60 @@
<section class="cru-private-cluster">
<div class="row">
<div class="col span-12">
<BannerMessage
@icon="icon-alert"
@color="bg-warning mb-10"
@message={{t
"clusterNew.googlegke.privateCluster.details"
appName=settings.appName
htmlSafe=true
}}
/>
</div>
</div>
<div class="row">
<div class="col span-6">
<div class="form-control-static">
<div class="checkbox">
<label>
{{input
type="checkbox"
checked=config.enablePrivateNodes
disabled=editing
}}
{{t "clusterNew.googlegke.enablePrivateNodes.label"}}
</label>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col span-6">
<div class="form-control-static">
<div class="checkbox">
<label>
{{input
type="checkbox"
checked=config.enablePrivateEndpoint
disabled=(or editing (not config.enablePrivateNodes))
}}
{{t "clusterNew.googlegke.privateCluster.privateEndpoint.label"}}
</label>
</div>
<p class="ml-25 help-block">
{{t "clusterNew.googlegke.privateCluster.privateEndpoint.help"}}
</p>
</div>
</div>
<div class="col span-6">
<label class="acc-label">
{{t "clusterNew.googlegke.masterIpv4CidrBlock.label"}}
</label>
{{input
value=config.masterIpv4CidrBlock
placeholder=(t "clusterNew.googlegke.masterIpv4CidrBlock.placeholder")
disabled=(or editing (not config.enablePrivateNodes))
}}
</div>
</div>
</section>

View File

@ -16,6 +16,7 @@ export default Component.extend({
progressStep: null,
cancel: null,
changeCloudCredential: null,
disableSave: false,
createLabel: 'saveCancel.create',
savingLabel: 'generic.loading',

View File

@ -51,7 +51,7 @@
{{else}}
{{#unless hideSave}}
{{save-cancel
saveDisabled=(unless (get primaryResource cloudCredentialKey) true)
saveDisabled=(or (unless (get primaryResource cloudCredentialKey) true) disableSave)
save=progressStep
cancel=cancel
createLabel=createLabel

View File

@ -0,0 +1,270 @@
import Component from '@ember/component';
import {
computed, get, observer, set, setProperties
} from '@ember/object';
import { on } from '@ember/object/evented';
import { next } from '@ember/runloop';
import { inject as service } from '@ember/service';
import { isEmpty } from '@ember/utils';
import { coerce, minor } from 'semver';
// import { coerceVersion } from 'shared/utils/parse-version';
import { sortableNumericSuffix } from 'shared/utils/util';
import layout from './template';
export default Component.extend({
google: service(),
serivceVersions: service('version-choices'),
layout,
cluster: null,
model: null,
nodeAdvanced: false,
taints: null,
oauthScopesSelection: null,
scopeConfig: null,
diskTypeContent: null,
imageTypeContent: null,
machineTypes: null,
nodeVersions: null,
controlPlaneVersion: null,
upgradeVersion: false,
showNodeUpgradePreventionReason: false,
init() {
this._super(...arguments);
const { model } = this;
setProperties(this, {
scopeConfig: {},
diskTypeContent: this.google.diskTypes,
imageTypeContent: this.google.imageTypesV2,
});
if (model) {
const { taints = [], } = model.config
let _taints = [];
if (taints) {
_taints = taints.map((t = '') => {
const splitEffect = t.split(':')
const splitLabel = (splitEffect[1] || '').split('=')
return {
effect: splitEffect[0],
key: splitLabel[0],
value: splitLabel[1],
}
});
}
set(this, 'taints', _taints)
if (!get(this, 'oauthScopesSelection')) {
const oauthScopes = get(model.config, 'oauthScopes')
const { oauthScopesSelection, scopeConfig } = this.google.unmapOauthScopes(oauthScopes);
set(this, 'oauthScopesSelection', oauthScopesSelection);
if (scopeConfig) {
set(this, 'scopeConfig', scopeConfig);
}
}
} else {
setProperties(this, {
oauthScopesSelection: this.google.oauthScopeOptions.DEFAULT,
scopeConfig: this.google.defaultScopeConfig,
labels: [],
taints: [],
});
}
},
actions: {
setNodeLabels(section) {
if (this.isDestroyed || this.isDestroying) {
return;
}
const out = []
for (let key in section) {
out.pushObject(`${ key }=${ section[key] }`)
}
set(this, 'model.config.labels', out);
},
},
autoscalingChanged: on('init', observer('model.autoscaling.enabled', function() {
if (this.isDestroyed || this.isDestroying) {
return;
}
const { model: { autoscaling } } = this;
if (!autoscaling?.enabled) {
next(this, () => {
if (this.isDestroyed || this.isDestroying) {
return;
}
if (!isEmpty(autoscaling?.minNodeCount)) {
set(this, 'model.autoscaling.minNodeCount', 0);
}
if (!isEmpty(autoscaling?.maxNodeCount)) {
set(this, 'model.autoscaling.maxNodeCount', 0);
}
});
}
})),
scopeConfigChanged: on('init', observer('scopeConfig', function() {
if (this.isDestroyed || this.isDestroying) {
return;
}
set(this.model.config, 'oauthScopes', this.google.mapOauthScopes(this.oauthScopesSelection, this.scopeConfig));
})),
taintsChanged: on('init', observer('taints.@each.{effect,key,value}', function() {
if (this.isDestroyed || this.isDestroying) {
return;
}
const taints = this.taints || []
if (taints.length > 0) {
set(this.model.config, 'taints', taints.map((t) => {
return `${ t.effect }:${ t.key }=${ t.value }`
}))
} else {
set(this.model.config, 'taints', [])
}
})),
originalClusterVersion: computed('originalCluster.gkeConfig.kubernetesVersion', 'originalCluster.gkeStatus.upstreamSpec.kubernetesVersion', function() {
if (!isEmpty(get(this, 'originalCluster.gkeConfig.kubernetesVersion'))) {
return get(this, 'originalCluster.gkeConfig.kubernetesVersion');
}
if (!isEmpty(get(this, 'originalCluster.gkeStatus.upstreamSpec.kubernetesVersion'))) {
return get(this, 'originalCluster.gkeStatus.upstreamSpec.kubernetesVersion');
}
return '';
}),
upgradeAvailable: computed('controlPlaneVersion', 'mode', 'model.version', 'originalClusterVersion', 'showNodeUpgradePreventionReason', function() {
const originalClusterVersion = get(this, 'originalClusterVersion');
const clusterVersion = get(this, 'controlPlaneVersion');
const nodeVersion = get(this, 'model.version');
const mode = get(this, 'mode');
const initalClusterMinorVersion = parseInt(minor(coerce(clusterVersion)), 10);
const initalNodeMinorVersion = parseInt(minor(coerce(nodeVersion)), 10);
const diff = initalClusterMinorVersion - initalNodeMinorVersion;
if (mode === 'edit') {
// we must upgrade the cluster first
if (originalClusterVersion !== clusterVersion) {
set(this, 'showNodeUpgradePreventionReason', true);
return false;
}
}
if (diff === 0 && get(this, 'showNodeUpgradePreventionReason')) {
set(this, 'showNodeUpgradePreventionReason', false);
}
return diff === 1;
}),
editingExistingNodePool: computed('cluster.gkeStatus.upstreamSpec.nodePools.@each.name', 'mode', 'model.name', function() {
const upstreamNodePools = get(this, 'cluster.gkeStatus.upstreamSpec.nodePools');
const iExist = (upstreamNodePools || []).findBy('name', this.model?.name || '');
if (this.mode === 'edit' && !isEmpty(iExist)) {
return true;
}
return false;
}),
isNewNodePool: computed('editingExistingNodePool', function() {
if (this.editingExistingNodePool) {
return false;
}
return true;
}),
editedMachineChoice: computed('model.config.machineType', 'machineChoices', function() {
return get(this, 'machineChoices').findBy('name', get(this, 'model.config.machineType'));
}),
machineChoices: computed('machineTypes.[]', function() {
let out = (get(this, 'machineTypes') || []).slice();
out.forEach((obj) => {
setProperties(obj, {
displayName: `${ obj.name } (${ obj.description })`,
group: obj.name.split('-')[0],
sortName: sortableNumericSuffix(obj.name),
})
});
return out.sortBy('sortName')
}),
// versionChoices: computed('nodeVersions.[]', 'controlPlaneVersion', 'mode', function() {
// // google gke console allows the node version to be anything less than master version
// const {
// nodeVersions,
// controlPlaneVersion,
// mode,
// } = this;
// const coerceedVersion = coerceVersion(controlPlaneVersion);
// const maxVersionRange = `<= ${ coerceedVersion }`;
// let newVersions = this.serivceVersions.parseCloudProviderVersionChoices(nodeVersions, controlPlaneVersion, mode, maxVersionRange);
// const controlPlaneVersionMatch = newVersions.findBy('value', controlPlaneVersion);
// if (!isEmpty(controlPlaneVersionMatch)) {
// set(controlPlaneVersionMatch, 'label', `${ controlPlaneVersionMatch.label } (control plane version)`);
// set(this, 'model.version', controlPlaneVersionMatch.value);
// const indexOfMatch = newVersions.indexOf(controlPlaneVersionMatch);
// if (indexOfMatch > 0) {
// // gke returns a semver like 1.17.17-gke.2800, 1.17.17-gke.3000
// // semver logic sorts these correctly but because we have to coerce the version, all versions in the 1.17.17 comebace
// // since they are sorted lets just find our CP master match index and cut everything off before that
// newVersions = newVersions.slice(indexOfMatch);
// }
// }
// return newVersions;
// }),
clusterVersionDidChange: on('init', observer('controlPlaneVersion', function() {
const { controlPlaneVersion, editing } = this;
if (controlPlaneVersion && !editing) {
set(this, 'model.version', controlPlaneVersion);
}
})),
shouldUpgradeVersion: on('init', observer('upgradeVersion', function() {
const { upgradeVersion } = this;
const clusterVersion = get(this, 'controlPlaneVersion');
const nodeVersion = get(this, 'model.version');
if (upgradeVersion && clusterVersion !== nodeVersion) {
set(this, 'model.version', clusterVersion);
}
})),
});

View File

@ -0,0 +1,309 @@
<div class="gke-node-pool-row box mt-20">
<div class="row">
<div class="pull-right">
<button
class="btn bg-transparent text-small vertical-middle"
type="button"
{{action removeNodePool model}}
>
{{t "clusterNew.googlegke.nodePool.remove"}}
</button>
</div>
</div>
<section class="node-details">
<h4 class="mb-0">
{{t "clusterNew.googlegke.nodePools.nodes.title"}}
</h4>
<hr />
<div class="col span-4">
<label class="acc-label">
{{t "clusterNew.googlegke.masterVersion.label"}}
</label>
{{!-- <NewSelect
@classNames="form-control"
@content={{versionChoices}}
@value={{mut model.version}}
/> --}}
{{#if upgradeAvailable}}
<div class="checkbox form-control-static">
<label class="acc-label">
<Input
@type="checkbox"
@checked={{mut upgradeVersion}}
@classNames="form-control"
/>
{{t
"nodeGroupRow.version.upgrade"
from=model.version
version=controlPlaneVersion
}}
</label>
</div>
{{else}}
<div>
{{model.version}}
{{#if showNodeUpgradePreventionReason}}
<div class="help-block">
{{t "nodeGroupRow.version.warning"}}
</div>
{{/if}}
</div>
{{/if}}
</div>
<div class="row">
<div class="col span-4">
<label class="acc-label">
{{t "clusterNew.googlegke.imageType.label"}}
</label>
<SearchableSelect
@content={{imageTypeContent}}
@classNames="form-control"
@value={{mut model.config.imageType}}
@localizedLabel={{true}}
/>
</div>
<div class="col span-4">
<label class="acc-label">
{{t "clusterNew.googlegke.machineType.label"}}
</label>
<InputOrDisplay
@editable={{isNewNodePool}}
@value={{editedMachineChoice.displayName}}
>
<NewSelect
@classNames="form-control"
@optionValuePath="name"
@optionLabelPath="displayName"
@optionGroupPath="group"
@content={{machineChoices}}
@value={{mut model.config.machineType}}
@prompt="clusterNew.googlegke.machineType.prompt"
@localizedPrompt={{true}}
/>
</InputOrDisplay>
</div>
</div>
<div class="row">
<div class="col span-4">
<label class="acc-label">
{{t "clusterNew.googlegke.diskType.label"}}
</label>
<InputOrDisplay
@editable={{isNewNodePool}}
@value={{model.config.diskType}}
>
<SearchableSelect
@content={{diskTypeContent}}
@classNames="form-control"
@value={{mut model.config.diskType}}
@localizedLabel={{true}}
@readOnly={{editingExistingNodePool}}
/>
</InputOrDisplay>
</div>
<div class="col span-4">
<label class="acc-label">
{{t "clusterNew.googlegke.diskSizeGb.label"}}
</label>
<div class="input-group">
{{#if editingExistingNodePool}}
{{model.config.diskSizeGb}}{{t "generic.gigabyte"}}
{{else}}
<InputNumber @min={{10}} @value={{model.config.diskSizeGb}} />
<span class="input-group-addon bg-default">
{{t "generic.gigabyte"}}
</span>
{{/if}}
</div>
</div>
<div class="col span-4">
<label class="acc-label">
{{t "clusterNew.googlegke.localSsdCount.label"}}
</label>
<div class="input-group">
{{#if editingExistingNodePool}}
{{model.config.localSsdCount}}{{t "generic.gigabyte"}}
{{else}}
<InputInteger
@min={{0}}
@value={{mut model.config.localSsdCount}}
/>
<span class="input-group-addon bg-default">
{{t "generic.gigabyte"}}
</span>
{{/if}}
</div>
</div>
</div>
<div class="row">
<div class="col span-6">
<div class="checkbox">
<label>
{{input
type="checkbox"
checked=model.config.preemptible
disabled=editingExistingNodePool
}}
{{t "clusterNew.googlegke.preemptible.label"}}
</label>
</div>
</div>
</div>
<FormGkeTaints @taints={{taints}} @editable={{isNewNodePool}} />
<div class="row mt-20">
<div class="col span-12 mb-0">
<label class="acc-label">
{{t "clusterNew.googlegke.nodeLabels.label"}}
</label>
<FormKeyValue
@initialMap={{labels}}
@changed={{action "setNodeLabels"}}
@addActionLabel="clusterNew.googlegke.nodeLabels.addAction"
@editing={{isNewNodePool}}
/>
</div>
</div>
</section>
<section class="group-details mt-30 mb-30">
<h4 class="mb-0">
{{t "clusterNew.googlegke.nodePools.groups.title"}}
</h4>
<hr />
<div class="row">
<div class="col span-6">
<label class="acc-label">
{{t "clusterNew.googlegke.nodePools.name.label"}}
{{#if (or isNewNodePool editingExistingNodePool)}}
{{field-required}}
{{/if}}
</label>
<InputOrDisplay @editable={{isNewNodePool}} @value={{model.name}}>
<Input
@type="text"
@value={{mut model.name}}
@classNames="form-control"
/>
</InputOrDisplay>
</div>
<div class="col span-3">
<label class="acc-label">
{{t "clusterNew.googlegke.nodePools.initialNodeCount.label"}}
</label>
<InputNumber
@value={{mut model.initialNodeCount}}
@min={{1}}
@classNames="form-control"
/>
</div>
<div class="col span-3">
<label class="acc-label">
{{t "clusterNew.googlegke.nodePools.maxPodsConstraint.label"}}
</label>
<InputNumber
@value={{mut model.maxPodsConstraint}}
@min={{1}}
@classNames="form-control"
/>
</div>
</div>
<div class="row">
<div class="col span-6 mt-25">
<div class="form-control-static">
<div class="checkbox">
<label>
{{input type="checkbox" checked=model.autoscaling.enabled}}
{{t "clusterNew.googlegke.clusterAddons.horizontalPodAutoscaling"
}}
</label>
</div>
</div>
<div class="form-control-static">
<div class="checkbox">
<label>
{{input type="checkbox" checked=model.management.autoRepair}}
{{t "clusterNew.googlegke.enableAutoRepair.label"}}
</label>
</div>
</div>
<div class="form-control-static">
<div class="checkbox">
<label>
{{input type="checkbox" checked=model.management.autoUpgrade}}
{{t "clusterNew.googlegke.enableAutoUpgrade.label"}}
</label>
</div>
</div>
</div>
{{#if model.autoscaling.enabled}}
<div class="col span-3">
<label class="acc-label" for="input-min-node-count">
{{t "clusterNew.googlegke.minNodeCount.label"}}
</label>
<InputNumber
id="input-min-node-count"
@value={{mut model.autoscaling.minNodeCount}}
@min={{1}}
@max={{model.autoscaling.minNodeCount}}
@classNames="form-control"
/>
</div>
<div class="col span-3">
<label class="acc-label" for="input-max-node-count">
{{t "clusterNew.googlegke.maxNodeCount.label"}}
</label>
<InputNumber
id="input-max-node-count"
@value={{mut model.autoscaling.maxNodeCount}}
@min={{model.autoscaling.minNodeCount}}
@classNames="form-control"
/>
</div>
{{/if}}
</div>
</section>
<AdvancedSection @advanced={{nodeAdvanced}}>
<div class="row mt-20">
<div class="col span-6">
<label class="acc-label">
{{t "clusterNew.googlegke.oauthScopes.label"}}
</label>
<div class="radio">
<label>
<RadioButton
@selection={{mut oauthScopesSelection}}
@value="default"
@disabled={{editingExistingNodePool}}
/>
{{t "clusterNew.googlegke.oauthScopes.default"}}
</label>
</div>
<div class="radio">
<label>
<RadioButton
@selection={{mut oauthScopesSelection}}
@value="full"
@disabled={{editingExistingNodePool}}
/>
{{t "clusterNew.googlegke.oauthScopes.full"}}
</label>
</div>
<div class="radio">
<label>
<RadioButton
@selection={{mut oauthScopesSelection}}
@value="custom"
@disabled={{editingExistingNodePool}}
/>
{{t "clusterNew.googlegke.oauthScopes.custom"}}
</label>
</div>
</div>
{{#if (eq oauthScopesSelection "custom")}}
<GkeAccessScope
@model={{scopeConfig}}
@readOnly={{editingExistingNodePool}}
/>
{{/if}}
</div>
</AdvancedSection>
</div>

View File

@ -15,6 +15,7 @@ export default Component.extend({
clusterAdmin: CLUSTER_ADMIN,
showClusterAdminWarning: true,
showEksClusterWarning: false,
showGkeClusterWarning: false,
loading: true,
token: null,

View File

@ -37,6 +37,17 @@
</div>
</BannerMessage>
{{/if}}
{{#if showGkeClusterWarning}}
<BannerMessage @color="bg-warning">
<div class="pt-15 pb-15">
{{t
"clusterNew.import.command.instructionGkeCluster"
appName=settings.appName
htmlSafe=true
}}
</div>
</BannerMessage>
{{/if}}
<div class="mt-20">
{{t
"clusterNew.import.command.instructions"

View File

@ -0,0 +1,53 @@
import Component from '@ember/component';
import ipaddr from 'ipaddr.js';
import layout from './template';
import { isEmpty } from '@ember/utils';
import { setProperties, observer } from '@ember/object';
export default Component.extend({
layout,
tag: 'span',
classNames: 'input-cidr-container',
inputStyles: '',
value: null,
invalid: false,
errorMessage: '',
disabled: null,
placeholder: '10.0.0.0/14',
actions: {
focusOutHandler() {
this.isInvalid();
},
},
resetErrorsOnType: observer('value', function() {
const { invalid, errorMessage } = this;
if (invalid && !isEmpty(errorMessage)) {
setProperties(this, {
invalid: false,
errorMessage: null,
});
}
}),
isInvalid() {
const { value } = this;
if (isEmpty(value)) {
return true;
}
try {
ipaddr.parseCIDR(value);
} catch (error) {
setProperties(this, {
invalid: true,
errorMessage: error.message.replace('ipaddr: ', ''),
});
}
}
});

View File

@ -0,0 +1,12 @@
<Input
@value={{value}}
@placeholder={{placeholder}}
@type="text"
@focus-out={{action "focusOutHandler"}}
@class={{if invalid "input-error" ""}}
@disabled={{disabled}}
@classNames={{inputStyles}}
/>
<span class="error-container text-error help-block {{if invalid '' 'hide'}}">
{{errorMessage}}
</span>

View File

@ -1,10 +1,12 @@
import Component from '@ember/component';
import {
computed, get, observer, set, setProperties
} from '@ember/object';
import { alias, equal } from '@ember/object/computed';
import { later } from '@ember/runloop';
import { inject as service } from '@ember/service';
import ModalBase from 'shared/mixins/modal-base';
import layout from './template';
import { alias, equal } from '@ember/object/computed';
import { get, setProperties, set, observer } from '@ember/object';
import { inject as service } from '@ember/service';
import { later } from '@ember/runloop';
export default Component.extend(ModalBase, {
intl: service(),
@ -24,7 +26,6 @@ export default Component.extend(ModalBase, {
cluster: alias('modalOpts.cluster'),
clusterProvider: alias('cluster.clusterProvider'),
canShowAddHost: alias('cluster.canShowAddHost'),
isImport: alias('cluster.eksDisplayEksImport'),
isCustom: equal('clusterProvider', 'custom'),
@ -74,6 +75,18 @@ export default Component.extend(ModalBase, {
}
}),
isImport: computed('cluster.{eksDisplayEksImport,gkeDisplayImport}', function() {
const { clusterProvider } = this;
if (clusterProvider === 'amazoneksv2') {
return this.cluster.eksDisplayEksImport;
} else if (clusterProvider === 'googlegkev2') {
return this.cluster.gkeDisplayImport;
}
return false;
}),
async loadToken() {
const { cluster } = this;

View File

@ -45,8 +45,9 @@
<ImportCommand
@loading={{loading}}
@token={{token}}
@showClusterAdminWarning={{not eksDisplayEksImport}}
@showEksClusterWarning={{eksDisplayEksImport}}
@showClusterAdminWarning={{not cluster.eksDisplayEksImport}}
@showEksClusterWarning={{cluster.eksDisplayEksImport}}
@showGkeClusterWarning={{cluster.gkeDisplayImport}}
/>
{{/if}}
{{/unless}}

View File

@ -0,0 +1,496 @@
import Service from '@ember/service';
import { inject as service } from '@ember/service';
import { get, set } from '@ember/object';
import { reject } from 'rsvp';
import { isEmpty } from '@ember/utils';
import { addQueryParams } from 'ui/utils/util';
export default Service.extend({
globalStore: service(),
maintenanceWindows: [
{
value: null,
label: 'Any Time',
},
{
value: '00:00',
label: '12:00AM',
},
{
value: '03:00',
label: '3:00AM',
},
{
value: '06:00',
label: '6:00AM',
},
{
value: '09:00',
label: '9:00AM',
},
{
value: '12:00',
label: '12:00PM',
},
{
value: '15:00',
label: '3:00PM',
},
{
value: '19:00',
label: '7:00PM',
},
{
value: '21:00',
label: '9:00PM',
},
],
imageTypes: [
{
label: 'clusterNew.googlegke.imageType.UBUNTU',
value: 'UBUNTU',
},
{
label: 'clusterNew.googlegke.imageType.COS',
value: 'COS'
},
],
imageTypesV2: [
{
label: 'clusterNew.googlegke.imageTypeV2.UBUNTU',
value: 'UBUNTU',
},
{
label: 'clusterNew.googlegke.imageTypeV2.UBUNTU_D',
value: 'UBUNTU_CONTAINERD',
},
{
label: 'clusterNew.googlegke.imageTypeV2.COS',
value: 'COS',
},
{
label: 'clusterNew.googlegke.imageTypeV2.COS_D',
value: 'COS_CONTAINERD',
},
{
label: 'clusterNew.googlegke.imageTypeV2.WINDOWS_LTSC',
value: 'WINDOWS_LTSC',
},
{
label: 'clusterNew.googlegke.imageTypeV2.WINDOWS_SAC',
value: 'WINDOWS_SAC',
},
],
diskTypes: [
{
label: 'clusterNew.googlegke.diskType.pd-standard',
value: 'pd-standard',
},
{
label: 'clusterNew.googlegke.diskType.pd-ssd',
value: 'pd-ssd',
}
],
regions: [
'asia-east1',
'asia-east2',
'asia-northeast1',
'asia-northeast2',
'asia-south1',
'asia-southeast1',
'australia-southeast1',
'europe-north1',
'europe-west1',
'europe-west2',
'europe-west3',
'europe-west4',
'europe-west6',
'northamerica-northeast1',
'southamerica-east1',
'us-central1',
'us-east1',
'us-east4',
'us-west1',
'us-west2'
],
defaultAuthScopes: [
'devstorage.read_only',
'logging.write',
'monitoring',
'servicecontrol',
'service.management.readonly',
'trace.append'
],
defaultZoneType: 'zonal',
defaultRegionType: 'regional',
defaultScopeConfig: {
userInfo: 'none',
computeEngine: 'none',
storage: 'devstorage.read_only',
taskQueue: 'none',
bigQuery: 'none',
cloudSQL: 'none',
cloudDatastore: 'none',
stackdriverLoggingAPI: 'logging.write',
stackdriverMonitoringAPI: 'monitoring',
cloudPlatform: 'none',
bigtableData: 'none',
bigtableAdmin: 'none',
cloudPub: 'none',
serviceControl: 'none',
serviceManagement: 'service.management.readonly',
stackdriverTrace: 'trace.append',
cloudSourceRepositories: 'none',
cloudDebugger: 'none'
},
oauthScopeOptions: {
DEFAULT: 'default',
FULL: 'full',
CUSTOM: 'custom'
},
googleAuthURLPrefix: 'https://www.googleapis.com/auth/',
googleFullAuthUrl: 'https://www.googleapis.com/auth/cloud-platform',
googleAuthDefaultURLs() {
return this.defaultAuthScopes.map((a) => `${ this.googleAuthURLPrefix }${ a }`);
},
getValueFromOauthScopes(oauthScopes, key, defaultValue) {
const filteredValues = oauthScopes
.filter((scope) => scope.indexOf(key) !== -1)
.map((scope) => {
return scope
.replace(this.googleAuthURLPrefix, '')
.replace(key, '').split('.')
})
.filter((splitScopes) => splitScopes.length <= 2);
if (filteredValues.length !== 1) {
return defaultValue || 'none';
}
return filteredValues[0].length === 1
? key
: `${ key }.${ filteredValues[0][1] }`;
},
/**
* This oauthScopesMapper is responsible for both the mapping to oauthScopes
* and unmapping from oauthscopes to form values. If you modify either
* method ensure that the other reflects your changes.
*/
mapOauthScopes(oauthScopesSelection, scopeConfig) {
if (oauthScopesSelection === this.oauthScopeOptions.DEFAULT) {
return this.googleAuthDefaultURLs();
} else if (oauthScopesSelection === this.oauthScopeOptions.FULL) {
return [this.googleFullAuthUrl];
} else if (oauthScopesSelection === this.oauthScopeOptions.CUSTOM) {
scopeConfig = scopeConfig || {};
let arr = [];
Object.keys(scopeConfig).map((key) => {
if (scopeConfig[key] !== 'none') {
arr.pushObject(`https://www.googleapis.com/auth/${ scopeConfig[key] }`)
}
})
return arr;
}
},
unmapOauthScopes(oauthScopes) {
const { getValueFromOauthScopes } = this;
const containsUrls = oauthScopes && oauthScopes.length > 0;
if (!containsUrls) {
return { oauthScopesSelection: this.oauthScopeOptions.DEFAULT };
}
const isAllAndOnlyDefaultUrls = ( this.googleAuthDefaultURLs().length === oauthScopes.length
&& this.googleAuthDefaultURLs().every((url) => oauthScopes.indexOf(url) !== -1) );
if (isAllAndOnlyDefaultUrls) {
return { oauthScopesSelection: this.oauthScopeOptions.DEFAULT }
}
const isOnlyTheFullUrl = oauthScopes.length === 1
&& oauthScopes[0] === this.googleFullAuthUrl;
if (isOnlyTheFullUrl) {
return { oauthScopesSelection: this.oauthScopeOptions.FULL }
}
return {
oauthScopesSelection: this.oauthScopeOptions.CUSTOM,
scopeConfig: {
userInfo: getValueFromOauthScopes(oauthScopes, 'userinfo', 'none'),
computeEngine: getValueFromOauthScopes(oauthScopes, 'compute', 'none'),
storage: getValueFromOauthScopes(oauthScopes, 'devstorage', 'devstorage.read_only'),
taskQueue: getValueFromOauthScopes(oauthScopes, 'taskqueue', 'none'),
bigQuery: getValueFromOauthScopes(oauthScopes, 'bigquery', 'none'),
cloudSQL: getValueFromOauthScopes(oauthScopes, 'sqlservice', 'none'),
cloudDatastore: getValueFromOauthScopes(oauthScopes, 'clouddatastore', 'none'),
stackdriverLoggingAPI: getValueFromOauthScopes(oauthScopes, 'logging', 'logging.write'),
stackdriverMonitoringAPI: getValueFromOauthScopes(oauthScopes, 'monitoring', 'monitoring'),
cloudPlatform: getValueFromOauthScopes(oauthScopes, 'cloud-platform', 'none'),
bigtableData: getValueFromOauthScopes(oauthScopes, 'bigtable.data', 'none'),
bigtableAdmin: getValueFromOauthScopes(oauthScopes, 'bigtable.admin', 'none'),
cloudPub: getValueFromOauthScopes(oauthScopes, 'pubsub', 'none'),
serviceControl: getValueFromOauthScopes(oauthScopes, 'servicecontrol', 'none'),
serviceManagement: getValueFromOauthScopes(oauthScopes, 'service.management', 'service.management.readonly'),
stackdriverTrace: getValueFromOauthScopes(oauthScopes, 'trace', 'trace.append'),
cloudSourceRepositories: getValueFromOauthScopes(oauthScopes, 'source', 'none'),
cloudDebugger: getValueFromOauthScopes(oauthScopes, 'cloud_debugger', 'none'),
}
};
},
parseProjectId(config) {
const str = get(config, 'credential');
if ( str ) {
try {
const obj = JSON.parse(str);
// Note: this is a Google project id, not ours.
const projectId = obj.project_id;
return projectId;
} catch (e) {
}
}
},
request(url, method, data) {
return this.globalStore.rawRequest({
url,
method,
data
});
},
parseRequestData(url, config) {
const {
googleCredentialSecret,
projectID: projectId,
region,
zone,
} = config;
const data = {};
if (!isEmpty(googleCredentialSecret)) {
set(data, 'cloudCredentialId', googleCredentialSecret);
if (!isEmpty(region)) {
set(data, 'region', region);
} else if (!isEmpty(zone)) {
set(data, 'zone', zone);
}
if (!isEmpty(projectId)) {
set(data, 'projectId', projectId);
}
}
return addQueryParams(url, data);
},
async fetchClusters(config, saved = false) {
if (saved) {
return;
}
const neuURL = this.parseRequestData('/meta/gkeClusters', config);
try {
const xhr = await this.request(neuURL, 'GET');
const out = xhr.body.clusters;
return out;
} catch (error) {
return reject([error.body.error ?? error.body]);
}
},
async fetchZones(config, saved = false) {
if (saved) {
return;
}
const neuURL = this.parseRequestData('/meta/gkeZones', config);
try {
const xhr = await this.request(neuURL, 'GET');
const out = xhr.body.items;
const locations = get(config, 'locations') || []
if (locations.length > 0) {
out.map((o) => {
if (locations.includes(o.name)) {
set(o, 'checked', true)
}
})
}
return out;
} catch (error) {
return reject([error.body.error ?? error.body]);
}
},
async fetchVersions(config, saved = false) {
if (saved) {
return;
}
const zone = get(config, 'zone') || `${ get(config, 'region') }-b`;
const neuConfig = { ...config };
delete neuConfig.region;
set(neuConfig, 'zone', zone);
const neuURL = this.parseRequestData('/meta/gkeVersions', neuConfig);
try {
const xhr = await this.request(neuURL, 'GET');
const out = xhr.body;
return out;
} catch (error) {
return reject([error.body.error ?? error.body]);
}
},
async fetchMachineTypes(config, saved = false) {
if (saved) {
return;
}
const zone = get(config, 'zone') || `${ get(config, 'region') }-b`;
const neuConfig = { ...config };
delete neuConfig.region;
set(neuConfig, 'zone', zone);
const neuURL = this.parseRequestData('/meta/gkeMachineTypes', neuConfig);
try {
const xhr = await this.request(neuURL, 'GET');
const out = xhr.body.items;
return out;
} catch (error) {
return reject([error.body.error ?? error.body]);
}
},
async fetchNetworks(config, saved = false) {
if (saved) {
return;
}
const zone = get(config, 'zone') || `${ get(config, 'region') }-b`;
const neuConfig = { ...config };
delete neuConfig.region;
set(neuConfig, 'zone', zone);
const neuURL = this.parseRequestData('/meta/gkeNetworks', neuConfig);
try {
const xhr = await this.request(neuURL, 'GET');
const out = xhr.body.items || [];
return out;
} catch (error) {
return reject([error.body.error ?? error.body]);
}
},
async fetchSubnetworks(config, locationType, saved = false) {
if (saved) {
return;
}
const region = locationType === this.defaultZoneType ? `${ config.zone.split('-')[0] }-${ config.zone.split('-')[1] }` : config.region;
const neuConfig = { ...config };
delete neuConfig.zone;
set(neuConfig, 'region', region);
const neuURL = this.parseRequestData('/meta/gkeSubnetworks', neuConfig);
try {
const xhr = await this.request(neuURL, 'GET');
const out = xhr.body.items || [];
return out;
} catch (error) {
return reject([error.body.error ?? error.body]);
}
},
async fetchSharedSubnets(config, saved = false) {
if (saved) {
return;
}
const neuConfig = { ...config };
delete neuConfig?.zone;
delete neuConfig?.region;
const neuURL = this.parseRequestData('/meta/gkeSharedSubnets', neuConfig);
try {
const xhr = await this.request(neuURL, 'GET');
const out = xhr?.body?.subnetworks || [];
// const out = [
// {
// 'ipCidrRange': '10.1.0.0/24',
// 'network': 'projects/vpc-host-309518/global/networks/vpc-host-network',
// 'secondaryIpRanges': [
// {
// 'ipCidrRange': '10.2.0.0/21',
// 'rangeName': 'pods',
// 'status': 'UNUSED'
// },
// {
// 'ipCidrRange': '10.3.0.0/21',
// 'rangeName': 'services',
// 'status': 'UNUSED'
// }
// ],
// 'subnetwork': 'projects/vpc-host-309518/regions/us-west1/subnetworks/vpc-host-subnet'
// }
// ];
return out;
} catch (error) {
return reject([error.body.error ?? error.body]);
}
},
async fetchServiceAccounts(config, saved = false) {
if (saved) {
return;
}
const zone = get(config, 'zone') || `${ get(config, 'region') }-b`;
const neuConfig = { ...config };
delete neuConfig.region;
set(neuConfig, 'zone', zone);
const neuURL = this.parseRequestData('/meta/gkeServiceAccounts', neuConfig);
try {
const xhr = await this.request(neuURL, 'GET');
const out = xhr.body.items || [];
return out;
} catch (error) {
return reject([error.body.error ?? error.body]);
}
},
});

View File

@ -24,7 +24,6 @@ export default Mixin.create({
init() {
this._super(...arguments);
defineProperty(this, 'config', computed('configField', `primaryResource.${ this.configField }`, this._getConfigField));
},

View File

@ -12,12 +12,13 @@ export default Service.extend({
defaultK8sVersionRange: alias(`settings.${ C.SETTING.VERSION_SYSTEM_K8S_DEFAULT_RANGE }`),
parseCloudProviderVersionChoices(versions, providerVersion, mode) {
parseCloudProviderVersionChoices(versions, providerVersion, mode, maxVersionRange = null) {
let {
intl,
defaultK8sVersionRange
} = this;
const maxVersionRange = defaultK8sVersionRange.split(' ').pop();
maxVersionRange = maxVersionRange ? maxVersionRange : defaultK8sVersionRange.split(' ').pop();
return versions.map((version) => {
if (satisfies(coerceVersion(version), maxVersionRange)) {

View File

@ -0,0 +1 @@
export { default } from 'shared/components/cluster-driver/driver-gke/component';

View File

@ -0,0 +1 @@
export { default } from 'shared/components/cluster-driver/driver-import-gke/component';

View File

@ -0,0 +1 @@
export { default } from 'shared/components/cru-master-auth-network/component';

View File

@ -0,0 +1 @@
export { default } from 'shared/components/cru-private-cluster/component';

View File

@ -0,0 +1 @@
export { default } from 'shared/components/gke-node-pool-row/component';

View File

@ -0,0 +1 @@
export { default } from 'shared/components/input-cidr/component';

View File

@ -0,0 +1 @@
export { default } from 'shared/google/service';

File diff suppressed because it is too large Load Diff