前言
說實話作爲一個後端語言的工程師來說,Spinnaker的Deck代碼是我研究起來最費勁的,因爲Spinnaker的UI設計並沒有採用antd之類的前端腳手架,而是純靠angular+typescript+html一點點寫出來的,不僅需要有這些js語言的基礎,還要找到項目內代碼之間的引用關係,對於js項目來說引用關係確實需要花時間一點點的去review代碼。不過經過努力最終還是啃下了,越是困難的事情,越是有收穫。
Review入口
Spinnaker所有跟雲廠商有關的微服務節點的目錄結構都是核心的放在core內,每家雲私有的部分放在以自己縮寫的目錄中,deck也遵循這個規則。以阿里云爲例,review入口從alicloud.module.tx中開始。
CloudProviderRegistry.registerProvider('alicloud', {
name: 'AlibabaCloud',
logo: {
path: require('./logo/alicloud.png'),
},
image: {
reader: 'alicloudImageReader',
},
serverGroup: {
transformer: 'alicloudServerGroupTransformer',
detailsTemplateUrl: require('./serverGroup/details/serverGroupDetails.html'),
detailsController: 'alicloudServerGroupDetailsCtrl',
// CloneServerGroupModal: CloneServerGroupAlicloud,
cloneServerGroupTemplateUrl: require('./serverGroup/configure/wizard/serverGroupWizard.html'),
cloneServerGroupController: 'alicloudCloneServerGroupCtrl',
commandBuilder: 'alicloudServerGroupCommandBuilder',
configurationService: 'alicloudServerGroupConfigurationService',
},
instance: {
instanceTypeService: 'alicloudInstanceTypeService',
detailsTemplateUrl: require('./instance/details/instanceDetails.html'),
detailsController: 'alicloudInstanceDetailsCtrl',
},
securityGroup: {
transformer: 'alicloudSecurityGroupTransformer',
writer: 'alicloudSecurityGroupWriter',
reader: 'alicloudSecurityGroupReader',
detailsTemplateUrl: require('./securityGroup/details/securityGroupDetail.html'),
detailsController: 'alicloudSecurityGroupDetailsCtrl',
// CreateSecurityGroupModal: CreateSecurityGroup,
createSecurityGroupTemplateUrl: require('./securityGroup/configure/createSecurityGroup.html'),
createSecurityGroupController: 'alicloudCreateSecurityGroupCtrl',
},
loadBalancer: {
transformer: 'alicloudLoadBalancerTransformer',
detailsTemplateUrl: require('./loadBalancer/details/loadBalancerDetail.html'),
detailsController: 'alicloudLoadBalancerDetailsCtrl',
// CreateLoadBalancerModal: ConfigureLoadBalancerModal,
createLoadBalancerTemplateUrl: require('./loadBalancer/configure/createLoadBalancer.html'),
createLoadBalancerController: 'alicloudCreateLoadBalancerCtrl',
},
});
此處註冊了阿里雲的服務,定義了名稱與圖標, 圖標的展示以及前端UI據大部分的展示已經在core中實現了,而這裏定義的都是自定義的部分。
image定義的是鏡像相關的內容,因爲在deploy和彈性伸縮的創建時需要與雲平臺交互取到鏡像列表的,這部分雲廠商接口都不一樣,所以要個性處理。爲什麼沒有writer呢?因爲Spinnaker的鏡像製作是又bake階段實現的,交給了rosco這個微服務節點,而rosco本質也是將http請求轉化爲packer命令,所以鏡像的製作是由packer來做的,spinnaker代碼本身並未涉及。
servergroup是spinnaker的核心對象,是專門處理彈性伸縮的,當然它並不是孤立的,無論哪家雲平臺,彈性伸縮部分也都需要與鏡像、實例類型、安全組、LoadBalance這些關聯的,所以servergroup並不能孤立的使用。
transformer代碼在serverGroup.transformer.ts,convertServerGroupCommandToDeployConfiguration方法將command轉化爲configuration,最終會被serverGroupWirter.ts調用。
detailsTemplateUrl和detailsController這是一對大寶貝,前者是右側詳細信息的html模板,後者是該模板的數據來源服務。功能位置:
核心代碼在serverGroupDetails.alicloud.controller.ts的retrieveServerGroup()方法,可見數據是從ServerGroupReader中來的:
function retrieveServerGroup() {
const summary = extractServerGroupSummary();
return ServerGroupReader.getServerGroup(
app.name,
serverGroup.accountId,
serverGroup.region,
serverGroup.name,
).then(function(details: any) {
cancelLoader();
angular.extend(details, summary);
details.account = serverGroup.accountId;
$scope.serverGroup = details;
if (!_.isEmpty($scope.serverGroup)) {
$scope.image = details.image ? details.image : undefined;
if (details.image && details.image.description) {
const tags: any[] = details.image.description.split(',');
tags.forEach(function(tag) {
const keyVal: any[] = tag.split('=');
if (keyVal.length === 2 && keyVal[0] === 'ancestor_name') {
details.image.baseImage = keyVal[1];
}
});
}
if (details.launchConfig && details.launchConfig.securityGroups) {
$scope.securityGroups = _.chain(details.launchConfig.securityGroups)
.map(function(id: any) {
return (
_.find(app.securityGroups.data, {
accountName: serverGroup.accountId,
region: serverGroup.region,
id: id,
}) ||
_.find(app.securityGroups.data, {
accountName: serverGroup.accountId,
region: serverGroup.region,
name: id,
})
);
})
.compact()
.value();
}
} else {
$state.go('^');
}
});
}
commandBuilder,js中的代理模式,此處不好理解,需要詳細說明,以buildServerGroupCommandFromPipeline爲例。
modules\alicloud\src\serverGroup\configure\serverGroupCommandBuilder.service.ts中定義了buildServerGroupCommandFromPipeline的實現,構造一個command複雜對象。
modules\core\src\serverGroup\configure\common\serverGroupCommandBuilder.service.ts中通過代理模式對其進行引用
modules\core\src\pipeline\config\stages\deploy\deployStage.js是真正的調用者
configurationService 實例類型、lb列表框、安全組列表框等配套服務,正如前文所說,彈性伸縮組與這些配套資源需要一起使用。
把ServerGroup的註冊部分看完,剩下的也就迎刃而解了instance、securityGroup、loadBalancer,都是類似的規則。
Pipeline原理
Spinnaker中對資源的增刪改查分爲兩種方式,第一種是INFRASTRUCTURE中對彈性伸縮、LoadBalance、安全組這三種資源的直接操作;第二種是將其封裝成一個個的固化的stage集成到pipeline流水線中。我們這裏看下pipeline的前端代碼是怎麼實現的。Pipeline的核心是Registry.pipeline.registerStage:註冊Stage。
以tagImageStage爲例代碼在modules\alicloud\src\pipeline\stages\tagImage\alicloudTagImageStage.js
Registry.pipeline.registerStage({
provides: 'upsertImageTags',
cloudProvider: 'alicloud',
templateUrl: require('./tagImageStage.html'),
executionDetailsUrl: require('./tagImageExecutionDetails.html'),
executionConfigSections: ['tagImageConfig', 'taskStatus'],
validators: [
{ type: 'requiredField', fieldName: 'region' },
{ type: 'requiredField', fieldName: 'tags' },
],
});
provides: stage的標識;cloudProvider: 雲平臺標識;templateUrl: stage的配置頁面;executionDetailsUrl:pipeline中stage詳情展示;executionConfigSections: pipeline執行信息中的導航;validators: form校驗。通過這些關聯關係的配置,將前面說講的對資源的操作封裝到了stage中來。
再來看這個環節註冊器Registry.ts的代碼,分析registerStage的細節,看到就2行代碼:
public registerStage(stageConfig: IStageTypeConfig): void {
this.stageTypes.push(stageConfig);
this.normalizeStageTypes();
}
stageTypes是IStageTypeConfig數組(IStageTypeConfig.ts中定義),normalizeStageTypes中有一個對parent的處理,具有parent的如果有些默認值爲空,可以從同provides的parent中繼承。
現在知道如何存的了,下面看如何查找的。pipelineConfigurer.html是pipeline配置頁面,聚焦pipelineConfigurerCtrl.addStage()方法,但這裏看到的只是流程圖的變更,並沒有定位到具體的stage頁面,一直找下去,其實在這裏stage.module.js
.directive('pipelineConfigStage', function() {
return {
restrict: 'E',
require: '^pipelineConfigurer',
scope: {
viewState: '=',
application: '=',
pipeline: '=',
stageFieldUpdated: '<',
},
controller: 'StageConfigCtrl as stageConfigCtrl',
templateUrl: require('./stage.html'),
link: function(scope, elem, attrs, pipelineConfigurerCtrl) {
scope.pipelineConfigurerCtrl = pipelineConfigurerCtrl;
},
};
})
跟入controller中,發現取的是options.stageTypes,核心代碼:$scope.options.stageTypes = Registry.pipeline.getConfigurableStageTypes(accounts);回到Registry.pipeline中找到相關代碼:
public getConfigurableStageTypes(accounts?: IAccountDetails[]): IStageTypeConfig[] {
const providers: string[] = isNil(accounts) ? [] : Array.from(new Set(accounts.map(a => a.cloudProvider)));
const allStageTypes = this.getStageTypes();
let configurableStageTypes = allStageTypes.filter(stageType => !stageType.synthetic && !stageType.provides);
if (providers.length === 0) {
return configurableStageTypes;
}
configurableStageTypes.forEach(
type => (type.cloudProviders = this.getCloudProvidersForStage(type, allStageTypes, accounts)),
);
configurableStageTypes = configurableStageTypes.filter(type => {
return !accounts.every(a => {
const p = CloudProviderRegistry.getProvider(a.cloudProvider, a.skin);
return isExcludedStageType(type, p);
});
});
return configurableStageTypes
.filter(stageType => stageType.cloudProviders.length)
.sort((a, b) => a.label.localeCompare(b.label));
}
Reader和Writer原理
以鏡像讀取reader爲例modules\core\src\image\image.reader.ts,看得出使用的是代理模式:
public findImages(params: IFindImageParams): IPromise<IImage[]> {
return this.getDelegate(params.provider).findImages(params);
}
private getDelegate(cloudProvider: string): IImageReader {
return this.providerServiceDelegate.getDelegate<IImageReader>(cloudProvider, 'image.reader');
}
服務的註冊和獲取在core/cloudProvider/providerService.delegate中:
public getDelegate<T>(provider: string, serviceKey: string, skin?: string): T {
const service = CloudProviderRegistry.getValue(provider, serviceKey, skin);
if (isString(service) && this.$injector.has(service)) {
// service is a string, it's an AngularJS service
return this.$injector.get<T>(service, 'providerDelegate');
} else if (isFunction(service)) {
// service is a Function, assume it's service class, so new() it
return new service();
} else {
throw new Error('No "' + serviceKey + '" service found for provider "' + provider + '"');
}
}
可見providerService這一層也只是邏輯層,真正存儲維護都交給了CloudProviderRegistry,其中有getValue方法,也必有setValue方法,而setValue方法的調用的內容就在剛開始介紹的alicloud.module.tx中。所以core中image.reader通過alicloud.module.tx的代理註冊,最終調用的是阿里雲自己項目中的image.reader。
瞭解了reader,writer以及其他service本質上都一樣了,都是core中通過代理調用了雲廠商私有的實現。
總結
1 ${clouddriver}.module.tx是最重要的入口,裏面的內容必須要掌握。
2 pipeline中代碼是對資源stage化的封裝,真正與後端的交互邏輯還是在資源本身的目錄中尋找。
3 core中通過代理模式獲取到雲平臺私有實現。
4 留意下modules\core\src\forms下的自定義組件,自己做功能擴展時優先用現成的,保持統一風格。