Skip to content

Commit ce8d378

Browse files
committed
feat(build): container publish for image with dacpac and sqlpackage
- paird with video demo: https://youtu.be/NaCNs0OOUbM - new build target PublishContainer will create a docker image that can be run to publish your project's dacpac to a target database via sqlpackage
1 parent 702a99c commit ce8d378

File tree

3 files changed

+130
-1
lines changed

3 files changed

+130
-1
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -340,7 +340,8 @@ ASALocalRun/
340340
healthchecksdb
341341

342342
# Exclude tools folder
343-
src/MSBuild.Sdk.SqlProj/tools/
343+
src/MSBuild.Sdk.SqlProj/tools/*
344+
!src/MSBuild.Sdk.SqlProj/tools/container/
344345

345346
# Exclude generated Dacpac's
346347
*.dacpac

src/MSBuild.Sdk.SqlProj/Sdk/Sdk.targets

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,4 +310,86 @@
310310
</ItemGroup>
311311
<Message Importance="Low" Text="CopyDacpacFiles: @(CopyDacpacFiles)" />
312312
</Target>
313+
314+
<!-- ===== Container publish (experimental) : configuration ===== -->
315+
<PropertyGroup>
316+
<!-- Container CLI to invoke (typically 'docker') -->
317+
<ContainerRuntime>docker</ContainerRuntime>
318+
319+
<!-- Dockerfile that builds the publisher image (installs sqlpackage, copies dacpacs) -->
320+
<ContainerDockerfile>$(MSBuildThisFileDirectory)..\tools\container\sqlpackage.Dockerfile</ContainerDockerfile>
321+
322+
<!-- Default image name and tag. Override name & tag in CI as needed.
323+
Lowercased and trimmed to comply with Docker naming rules. -->
324+
<ContainerImageName Condition="'$(ContainerImageName)'==''">$([System.String]::Copy('$(MSBuildProjectName)').ToLowerInvariant().Trim())-publisher</ContainerImageName>
325+
<ContainerImageTag Condition="'$(ContainerImageTag)'==''">$([System.String]::Copy('$(Configuration)').ToLowerInvariant().Trim())</ContainerImageTag>
326+
327+
<!-- Passed to the Dockerfile as build-arg SQLPACKAGE_VERSION -->
328+
<SqlPackageVersion>170.2.70</SqlPackageVersion>
329+
330+
<!-- Local staging area for the project dacpac and referenced dacpacs -->
331+
<ContainerStagingDir>$(MSBuildProjectDirectory)\.container</ContainerStagingDir>
332+
<ContainerDacpacName>$(MSBuildProjectName).dacpac</ContainerDacpacName>
333+
<ContainerDacpacPath>$(ContainerStagingDir)\$(ContainerDacpacName)</ContainerDacpacPath>
334+
</PropertyGroup>
335+
336+
<!-- ===== Container publish (experimental) : targets ===== -->
337+
338+
<!--
339+
PrepareContainerDacpac
340+
Purpose: Build and stage the project dacpac and place all referenced dacpacs next to it for convenience.
341+
Runs after a successful Build
342+
Guards: Fails fast if the dacpac or Dockerfile is missing.
343+
Inputs: $(TargetPath) (built dacpac), @(IncludedDacpacReferenceFiles)
344+
Outputs/Side effects: Recreates $(ContainerStagingDir), copies dacpac(s) into it.
345+
-->
346+
<Target Name="PrepareContainerDacpac"
347+
DependsOnTargets="Build;ResolveDatabaseReferences">
348+
349+
<Error Condition="!Exists('$(TargetPath)')"
350+
Text="DACPAC '$(TargetPath)' not found. Build failed." />
351+
<Error Condition="!Exists('$(ContainerDockerfile)')"
352+
Text="Dockerfile '$(ContainerDockerfile)' not found. Check ContainerDockerfile property." />
353+
354+
<!-- Reset staging area -->
355+
<RemoveDir Directories="$(ContainerStagingDir)" Condition="Exists('$(ContainerStagingDir)')" />
356+
<MakeDir Directories="$(ContainerStagingDir)" />
357+
358+
<!-- Copy the project dacpac to the known staging path -->
359+
<Copy SourceFiles="$(TargetPath)"
360+
DestinationFiles="$(ContainerDacpacPath)"
361+
OverWriteReadOnlyFiles="true" />
362+
363+
<!-- Also stage all referenced dacpacs beside the project dacpac (useful for Docker COPY patterns) -->
364+
<Copy SourceFiles="@(IncludedDacpacReferenceFiles)"
365+
DestinationFolder="$(ContainerStagingDir)"
366+
SkipUnchangedFiles="true" />
367+
</Target>
368+
369+
<!--
370+
PublishContainer
371+
Purpose: Build the publisher container image from the staged dacpac(s).
372+
Runs after PrepareContainerDacpac
373+
Behavior: Executes '<runtime> build' with tag and SQLPACKAGE_VERSION build-arg.
374+
Output: Local container image $(ContainerImageName):$(ContainerImageTag)
375+
-->
376+
<Target Name="PublishContainer"
377+
DependsOnTargets="PrepareContainerDacpac">
378+
<Message Text="Building container $(ContainerImageName):$(ContainerImageTag)"
379+
Importance="high" />
380+
<Exec Command='"$(ContainerRuntime)" build -f "$(ContainerDockerfile)" -t "$(ContainerImageName):$(ContainerImageTag)" --build-arg SQLPACKAGE_VERSION=$(SqlPackageVersion) --build-arg DACPAC_NAME=$(ContainerDacpacName) "$(MSBuildProjectDirectory)"'
381+
IgnoreExitCode="false" />
382+
</Target>
383+
384+
<!--
385+
PushContainer
386+
Pushes to the registry implied by ContainerImageName (e.g., 'ghcr.io/org/name').
387+
Ensure you've run 'docker login' (or equivalent) for that registry first.
388+
-->
389+
<Target Name="PushContainer">
390+
<Message Text="Pushing container $(ContainerImageName):$(ContainerImageTag)"
391+
Importance="high" />
392+
<Exec Command='"$(ContainerRuntime)" push "$(ContainerImageName):$(ContainerImageTag)"'
393+
IgnoreExitCode="false" />
394+
</Target>
313395
</Project>
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Base image: ships with the SDK
2+
ARG BASE_IMAGE=mcr.microsoft.com/dotnet/runtime:8.0
3+
FROM ${BASE_IMAGE}
4+
ARG SQLPACKAGE_VERSION
5+
ARG DACPAC_NAME
6+
ENV DACPAC_NAME=${DACPAC_NAME}
7+
8+
# Install sqlpackage dependencies and pull from NuGet
9+
# - Supports native (platform zip) and dotnet tool layouts
10+
# - Installs a stable wrapper in /usr/local/bin/sqlpackage
11+
RUN set -eux; \
12+
apt-get update \
13+
&& apt-get install -y --no-install-recommends ca-certificates curl unzip \
14+
&& curl -L -o /tmp/sqlpackage.nupkg "https://globalcdn.nuget.org/packages/microsoft.sqlpackage.${SQLPACKAGE_VERSION}.nupkg" \
15+
&& mkdir -p /opt/sqlpackage-src /opt/sqlpackage \
16+
&& unzip -q /tmp/sqlpackage.nupkg -d /opt/sqlpackage-src \
17+
&& inner="$(find /opt/sqlpackage-src -type f -iname '*linux-x64*.zip' | head -n1 || true)" \
18+
&& if [ -n "$inner" ]; then \
19+
unzip -q "$inner" -d /opt/sqlpackage; \
20+
else \
21+
cp -R /opt/sqlpackage-src/tools/*/any/* /opt/sqlpackage/; \
22+
fi \
23+
&& printf '#!/usr/bin/env bash\nset -euo pipefail\nif [ -x /opt/sqlpackage/sqlpackage ]; then exec /opt/sqlpackage/sqlpackage "$@"; elif [ -f /opt/sqlpackage/sqlpackage.dll ]; then exec dotnet /opt/sqlpackage/sqlpackage.dll "$@"; else echo "sqlpackage not found"; exit 127; fi\n' > /usr/local/bin/sqlpackage \
24+
&& chmod +x /usr/local/bin/sqlpackage \
25+
&& rm -rf /var/lib/apt/lists/* /tmp/* /opt/sqlpackage-src \
26+
# Create entrypoint wrapper as root:
27+
# - Bakes in `-Action:Publish` and `-SourceFile:/work/$DACPAC_NAME`
28+
# - Forwards user-supplied args to sqlpackage
29+
&& install -d /usr/local/bin \
30+
&& printf '%s\n' \
31+
'#!/usr/bin/env bash' \
32+
'set -euo pipefail' \
33+
'exec sqlpackage -Action:Publish -SourceFile:/work/"${DACPAC_NAME}" "$@"' \
34+
| tee /usr/local/bin/docker-entrypoint >/dev/null \
35+
&& chmod +x /usr/local/bin/docker-entrypoint
36+
37+
# Staging: the SDK copies all dacpacs into /work at build time
38+
WORKDIR /work
39+
COPY ./.container/*.dacpac /work/
40+
41+
# Drop privileges to a non-root user
42+
RUN useradd -m runner
43+
USER runner
44+
45+
# Exec-form ENTRYPOINT for proper signal handling and linter compliance
46+
ENTRYPOINT ["/usr/local/bin/docker-entrypoint"]

0 commit comments

Comments
 (0)