diff --git a/extensions/sql-database-projects/src/common/constants.ts b/extensions/sql-database-projects/src/common/constants.ts index 743e829148..53f178946e 100644 --- a/extensions/sql-database-projects/src/common/constants.ts +++ b/extensions/sql-database-projects/src/common/constants.ts @@ -194,6 +194,7 @@ export function invalidSQLPasswordMessage(name: string) { return localize('inval export function passwordNotMatch(name: string) { return localize('passwordNotMatch', "{0} password doesn't match the confirmation password", name); } export const portMustBeNumber = localize('portMustNotBeNumber', "Port must a be number"); export const valueCannotBeEmpty = localize('valueCannotBeEmpty', "Value cannot be empty"); +export const imageTag = localize('imageTag', "Image tag"); export const dockerImageLabelPrefix = 'source=sqldbproject'; export const dockerImageNamePrefix = 'sqldbproject'; export const dockerImageDefaultTag = 'latest'; diff --git a/extensions/sql-database-projects/src/dialogs/publishDatabaseDialog.ts b/extensions/sql-database-projects/src/dialogs/publishDatabaseDialog.ts index 68ab5c587f..95dd003707 100644 --- a/extensions/sql-database-projects/src/dialogs/publishDatabaseDialog.ts +++ b/extensions/sql-database-projects/src/dialogs/publishDatabaseDialog.ts @@ -7,17 +7,18 @@ import type * as azdataType from 'azdata'; import * as vscode from 'vscode'; import * as constants from '../common/constants'; import * as utils from '../common/utils'; +import * as uiUtils from './utils'; import { Project } from '../models/project'; import { SqlConnectionDataSource } from '../models/dataSources/sqlConnectionStringSource'; import { DeploymentOptions } from 'mssql'; import { IconPathHelper } from '../common/iconHelper'; import { cssStyles } from '../common/uiConstants'; -import { getAgreementDisplayText, getConnectionName, getDefaultDockerImageWithTag, getDockerBaseImages, getPublishServerName } from './utils'; +import { getAgreementDisplayText, getConnectionName, getDockerBaseImages, getPublishServerName } from './utils'; import { TelemetryActions, TelemetryReporter, TelemetryViews } from '../common/telemetry'; import { Deferred } from '../common/promise'; import { PublishOptionsDialog } from './publishOptionsDialog'; -import { ISqlProjectPublishSettings, IPublishToDockerSettings, SqlTargetPlatform } from 'sqldbproj'; +import { ISqlProjectPublishSettings, IPublishToDockerSettings } from 'sqldbproj'; interface DataSourceDropdownValue extends azdataType.CategoryValue { dataSource: SqlConnectionDataSource; @@ -46,6 +47,7 @@ export class PublishDatabaseDialog { private databaseRow: azdataType.FlexContainer | undefined; private localDbSection: azdataType.FlexContainer | undefined; private baseDockerImageDropDown: azdataType.DropDownComponent | undefined; + private imageTagDropDown: azdataType.DropDownComponent | undefined; private serverAdminPasswordTextBox: azdataType.InputBoxComponent | undefined; private serverConfigAdminPasswordTextBox: azdataType.InputBoxComponent | undefined; private serverPortTextBox: azdataType.InputBoxComponent | undefined; @@ -116,7 +118,7 @@ export class PublishDatabaseDialog { const flexRadioButtonsModel = this.createPublishTypeRadioButtons(view); // TODO : enable using this when data source creation is enabled this.createRadioButtons(view); - this.createLocalDbInfoRow(view); + await this.createLocalDbInfoRow(view); this.dataSourcesFormComponent = this.createDataSourcesFormComponent(view); @@ -244,10 +246,13 @@ export class PublishDatabaseDialog { let dockerBaseImage = this.getBaseDockerImageName(); const baseImages = getDockerBaseImages(this.project.getProjectTargetVersion()); const imageInfo = baseImages.find(x => x.name === dockerBaseImage); + const imageName = imageInfo?.name; + const imageTag = this.imageTagDropDown?.value; - // selecting the image tag isn't currently exposed in the publish dialog, so this adds the tag matching the target platform - // to make sure the correct image is used for the project's target platform when the docker base image is SQL Server - dockerBaseImage = getDefaultDockerImageWithTag(this.project.getProjectTargetVersion(), dockerBaseImage, imageInfo); + // Add the image tag if it's not the latest + if (imageTag && imageTag !== constants.dockerImageDefaultTag) { + dockerBaseImage = `${imageName}:${imageTag}`; + } const settings: IPublishToDockerSettings = { dockerSettings: { @@ -554,7 +559,7 @@ export class PublishDatabaseDialog { return connectionRow; } - private createLocalDbInfoRow(view: azdataType.ModelView): azdataType.FlexContainer { + private async createLocalDbInfoRow(view: azdataType.ModelView): Promise { const name = getPublishServerName(this.project.getProjectTargetVersion()); this.serverPortTextBox = view.modelBuilder.inputBox().withProps({ value: constants.defaultPortNumber, @@ -604,15 +609,6 @@ export class PublishDatabaseDialog { const baseImages = getDockerBaseImages(this.project.getProjectTargetVersion()); const baseImagesValues: azdataType.CategoryValue[] = baseImages.map(x => { return { name: x.name, displayName: x.displayName }; }); - // add preview string for 2022 - // TODO: remove after 2022 is GA - if (this.project.getProjectTargetVersion() === constants.targetPlatformToVersion.get(SqlTargetPlatform.sqlServer2022)) { - const sqlServerImageIndex = baseImagesValues.findIndex(image => image.displayName === constants.SqlServerDockerImageName); - if (sqlServerImageIndex >= 0) { - baseImagesValues[sqlServerImageIndex].displayName = constants.SqlServerDocker2022ImageName; - } - } - this.baseDockerImageDropDown = view.modelBuilder.dropDown().withProps({ values: baseImagesValues, ariaLabel: constants.baseDockerImage(name), @@ -620,8 +616,28 @@ export class PublishDatabaseDialog { enabled: true }).component(); + const imageInfo = baseImages.find(x => x.displayName === (this.baseDockerImageDropDown?.value)?.displayName); + const imageTags = await uiUtils.getImageTags(imageInfo!, this.project.getProjectTargetVersion(), true); + + this.imageTagDropDown = view.modelBuilder.dropDown().withProps({ + values: imageTags, + value: imageTags[0], + ariaLabel: constants.imageTag, + width: cssStyles.publishDialogTextboxWidth, + enabled: true, + editable: true, + required: true, + fireOnTextChange: true + }).component(); + + this.imageTagDropDown.onValueChanged(() => { + this.tryEnableGenerateScriptAndOkButtons(); + }); + const agreementInfo = baseImages[0].agreementInfo; - const dropDownRow = this.createFormRow(view, constants.baseDockerImage(name), this.baseDockerImageDropDown); + const baseImageDropDownRow = this.createFormRow(view, constants.baseDockerImage(name), this.baseDockerImageDropDown); + const imageTagDropDownRow = this.createFormRow(view, constants.imageTag, this.imageTagDropDown); + this.eulaCheckBox = view.modelBuilder.checkBox().withProps({ ariaLabel: getAgreementDisplayText(agreementInfo), required: true @@ -633,8 +649,8 @@ export class PublishDatabaseDialog { const eulaRow = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'row', alignItems: 'center' }).component(); this.localDbSection = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component(); - this.localDbSection.addItems([serverPortRow, serverPasswordRow, serverConfirmPasswordRow, dropDownRow, eulaRow]); - this.baseDockerImageDropDown.onValueChanged(() => { + this.localDbSection.addItems([serverPortRow, serverPasswordRow, serverConfirmPasswordRow, baseImageDropDownRow, imageTagDropDownRow, eulaRow]); + this.baseDockerImageDropDown.onValueChanged(async () => { if (this.eulaCheckBox) { this.eulaCheckBox.checked = false; } @@ -651,6 +667,12 @@ export class PublishDatabaseDialog { eulaRow?.addItems([this.eulaCheckBox, text], { flex: '0 0 auto', CSSStyles: { 'margin-right': '10px' } }); } } + + // update image tag dropdown with the image tags for the selected base image + const imageInfo = baseImages.find(x => x.displayName === baseImage?.displayName); + const imageTags = await uiUtils.getImageTags(imageInfo!, this.project.getProjectTargetVersion(), true); + this.imageTagDropDown!.values = imageTags; + this.imageTagDropDown!.value = imageTags[0]; }); return this.localDbSection; } @@ -906,7 +928,7 @@ export class PublishDatabaseDialog { !utils.isEmptyString(this.serverAdminPasswordTextBox?.value) && utils.isValidSQLPassword(this.serverAdminPasswordTextBox?.value || '', constants.defaultLocalServerAdminName) && this.serverAdminPasswordTextBox?.value === this.serverConfigAdminPasswordTextBox?.value - && this.eulaCheckBox?.checked) { + && this.imageTagDropDown!.value && this.eulaCheckBox?.checked) { publishEnabled = true; // only publish is supported for container } diff --git a/extensions/sql-database-projects/src/dialogs/utils.ts b/extensions/sql-database-projects/src/dialogs/utils.ts index 34a93ba82b..f607b66805 100644 --- a/extensions/sql-database-projects/src/dialogs/utils.ts +++ b/extensions/sql-database-projects/src/dialogs/utils.ts @@ -53,9 +53,10 @@ export function getDockerImagePlaceHolder(target: string): string { * Returns the list of image tags for given target * @param imageInfo docker image info * @param target project target version + * @param defaultTagFirst whether the default tag should be the first entry in the array * @returns image tags */ -export async function getImageTags(imageInfo: DockerImageInfo, target: string): Promise { +export async function getImageTags(imageInfo: DockerImageInfo, target: string, defaultTagFirst?: boolean): Promise { let imageTags: string[] | undefined = []; const versionToImageTags: Map = new Map(); @@ -87,6 +88,14 @@ export async function getImageTags(imageInfo: DockerImageInfo, target: string): imageTags = imageTags ?? []; imageTags = imageTags.sort((a, b) => a.indexOf(constants.dockerImageDefaultTag) > 0 ? -1 : a.localeCompare(b)); + + if (defaultTagFirst) { + const defaultIndex = imageTags.findIndex(i => i === imageInfo.defaultTag); + if (defaultIndex > -1) { + imageTags.splice(defaultIndex, 1); + imageTags.unshift(imageInfo.defaultTag); + } + } } } catch (err) { // Ignore the error. If http request fails, we just use the default tag