Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

## Overview

The sqlserver module installs and manages Microsoft SQL Server 2014, 2016, 2017, 2019 and 2022 on Windows systems.
The sqlserver module installs and manages Microsoft SQL Server 2014, 2016, 2017, 2019, 2022 and 2025 on Windows systems.

## Module Description

Expand Down Expand Up @@ -273,11 +273,11 @@ For information on the classes and types, see the [REFERENCE.md](https://github.

## Limitations

SQL 2017, 2019 and 2022 detection support has been added. This support is limited to functionality already present for other versions.
SQL 2017, 2019, 2022 and 2025 detection support has been added. This support is limited to functionality already present for other versions.

The MSOLEDBSQL driver is now required to use this module. You can use this chocolatey [package](https://community.chocolatey.org/packages/msoledbsql) for installation. but it must version 18.x or earlier. (v19+ is not currently supported)
The MSOLEDBSQL driver is required to use this module. You can use this chocolatey [package](https://community.chocolatey.org/packages/msoledbsql) for installation.
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The text "now required" was changed to "required" which removes important information about timing. The original phrasing "now required" implied this was a recent change that users should be aware of. The new phrasing "required" makes it sound like it has always been required. Consider keeping "now required" or adding context about when this became a requirement to help users understand the change.

Suggested change
The MSOLEDBSQL driver is required to use this module. You can use this chocolatey [package](https://community.chocolatey.org/packages/msoledbsql) for installation.
The MSOLEDBSQL driver is now required to use this module. You can use this chocolatey [package](https://community.chocolatey.org/packages/msoledbsql) for installation.

Copilot uses AI. Check for mistakes.

This module can manage only a single version of SQL Server on a given host (one and only one of SQL Server 2014, 2016, 2017, 2019 or 2022). The module is able to manage multiple SQL Server instances of the same version.
This module can manage only a single version of SQL Server on a given host (one and only one of SQL Server 2014, 2016, 2017, 2019, 2022 or 2025). The module is able to manage multiple SQL Server instances of the same version.

This module cannot manage the SQL Server Native Client SDK (also known as SNAC_SDK). The SQL Server installation media can install the SDK, but it is not able to uninstall the SDK. Note that the 'sqlserver_features' fact detects the presence of the SDK.

Expand Down
16 changes: 10 additions & 6 deletions lib/puppet_x/sqlserver/features.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@
SQL_2017 = 'SQL_2017'
SQL_2019 = 'SQL_2019'
SQL_2022 = 'SQL_2022'
SQL_2025 = 'SQL_2025'

ALL_SQL_VERSIONS = [SQL_2014, SQL_2016, SQL_2017, SQL_2019, SQL_2022].freeze
ALL_SQL_VERSIONS = [SQL_2014, SQL_2016, SQL_2017, SQL_2019, SQL_2022, SQL_2025].freeze

# rubocop:disable Style/ClassAndModuleChildren
module PuppetX
Expand All @@ -35,12 +36,15 @@ class Features # rubocop:disable Style/Documentation
major_version: 15,
registry_path: '150'
},
SQL_2022 => {
major_version: 16,
registry_path: '160'
}
SQL_2022 => {
major_version: 16,
registry_path: '160'
},
SQL_2025 => {
major_version: 17,
registry_path: '170'
}
}.freeze

SQL_REG_ROOT = 'Software\Microsoft\Microsoft SQL Server'
HKLM = 'HKEY_LOCAL_MACHINE'

Expand Down
1 change: 1 addition & 0 deletions lib/puppet_x/sqlserver/server_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ def self.sql_version_from_install_source(source_dir)
ver = content.match('"(.+)"')
return nil if ver.nil?

return SQL_2025 if ver[1].start_with?('17.')
return SQL_2022 if ver[1].start_with?('16.')
return SQL_2019 if ver[1].start_with?('15.')
return SQL_2017 if ver[1].start_with?('14.')
Expand Down
3 changes: 2 additions & 1 deletion metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "puppetlabs-sqlserver",
"version": "5.0.5",
"author": "puppetlabs",
"summary": "The `sqlserver` module installs and manages MS SQL Server 2014, 2016, 2017, 2019 and 2022 on Windows systems.",
"summary": "The `sqlserver` module installs and manages MS SQL Server 2014, 2016, 2017, 2019, 2022 and 2025 on Windows systems.",
"license": "proprietary",
"source": "https://github.com/puppetlabs/puppetlabs-sqlserver",
"project_page": "https://github.com/puppetlabs/puppetlabs-sqlserver",
Expand Down Expand Up @@ -45,6 +45,7 @@
"sql2017",
"sql2019",
"sql2022",
"sql2025",
"tsql",
"database"
],
Expand Down
106 changes: 106 additions & 0 deletions spec/acceptance/manifests/install_oledb_driver.pp
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# frozen_string_literal: true
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first line contains a Ruby comment marker "# frozen_string_literal: true" in a Puppet manifest file. Puppet manifest files use their own comment syntax and do not recognize Ruby comment markers. This line should either be removed or changed to use Puppet's comment syntax.

Suggested change
# frozen_string_literal: true
# Acceptance manifest for installing the Microsoft OLE DB Driver for SQL Server

Copilot uses AI. Check for mistakes.

# Install the Microsoft OLE DB Driver for SQL Server via Chocolatey
# Using Puppet Exec with PowerShell provider for reliability

# Desired minimum version for MSOLEDBSQL; update as needed
$desired_version = '19.2.23273.0'
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable $desired_version is declared but never used anywhere in the manifest. The actual version used is hardcoded to '18.6.0.0' on line 21. Either this variable should be removed, or the hardcoded version should be replaced with this variable.

Copilot uses AI. Check for mistakes.

exec { 'install_chocolatey':
provider => 'powershell',
command => "Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))",
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The installation script uses 'iex' (Invoke-Expression) to execute code downloaded from the internet on line 11. While this is the standard Chocolatey installation method, it poses a security risk as it executes arbitrary code. The script does use HTTPS and sets SecurityProtocol to TLS 1.2 (3072), which is good. However, consider adding a comment explaining this security consideration and why it's acceptable for this test environment, or consider alternative installation methods that don't require executing downloaded scripts directly.

Copilot uses AI. Check for mistakes.
unless => "Test-Path 'C:\\ProgramData\\chocolatey\\choco.exe'",
tries => 3,
try_sleep=> 10,
timeout => 600,
}

$oledb_install_script = @(EOT)
$choco = 'C:\ProgramData\chocolatey\bin\choco.exe'
$reg = 'HKLM:\SOFTWARE\Microsoft\MSOLEDBSQL'
$ver = '18.6.0.0'
$pkgUrl = "https://community.chocolatey.org/api/v2/package/msoledbsql/$ver"

function Test-Installed {
try {
$v = (Get-ItemProperty $reg -ErrorAction SilentlyContinue).InstalledVersion
return ($v -and $v -like '18.*')
} catch { return $false }
}

function Invoke-Retry([scriptblock] $action, [int] $retries = 3, [int] $delay = 10) {
for ($i = 1; $i -le $retries; $i++) {
try {
& $action
return $true
} catch {
Write-Host "[msoledbsql] Attempt $i failed: $($_.Exception.Message)"
if ($i -lt $retries) { Start-Sleep -Seconds $delay }
}
}
return $false
}

Write-Host "[msoledbsql] Target version: $ver"
if (Test-Installed) {
Write-Host "[msoledbsql] Already installed (v18.x)."
exit 0
}

# Remove incompatible v19+ if present
try {
$v = (Get-ItemProperty $reg -ErrorAction SilentlyContinue).InstalledVersion
if ($v -and $v -like '19.*') {
Write-Host "[msoledbsql] Uninstalling incompatible v19.x: $v"
& $choco uninstall msoledbsql -y | Out-Host
Start-Sleep -Seconds 5
}
} catch { }

# Primary install from Chocolatey community feed
Invoke-Retry {
Write-Host "[msoledbsql] Installing from Chocolatey feed..."
& $choco install msoledbsql --version $ver -y --force --source 'https://community.chocolatey.org/api/v2/' --no-progress | Out-Host
if (-not (Test-Installed)) { throw "Install from feed did not register in registry" }
} 3 15 | Out-Null
Start-Sleep -Seconds 10

# Fallback: download nupkg directly and install from file
if (-not (Test-Installed)) {
try {
Write-Host "[msoledbsql] Falling back to direct package download: $pkgUrl"
$nupkg = "$env:TEMP\msoledbsql.$ver.nupkg"
Invoke-WebRequest -Uri $pkgUrl -OutFile $nupkg -UseBasicParsing
Invoke-Retry {
& $choco install $nupkg -y --force --no-progress | Out-Host
if (-not (Test-Installed)) { throw "Install from nupkg did not register in registry" }
} 3 15 | Out-Null
Start-Sleep -Seconds 10
} catch { Write-Host "[msoledbsql] Fallback install failed: $($_.Exception.Message)" }
}

if (Test-Installed) {
Write-Host "[msoledbsql] Install succeeded."
exit 0
} else {
Write-Host "[msoledbsql] Install failed."
exit 1
}
EOT

exec { 'install_oledb_driver':
provider => 'powershell',
command => $oledb_install_script,
require => Exec['install_chocolatey'],
unless => @(EOT)
try {
$v = (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\MSOLEDBSQL' -ErrorAction SilentlyContinue).InstalledVersion
if ($v -and $v -like '18.*') { exit 0 } else { exit 1 }
} catch { exit 1 }
EOT
,
Comment on lines +100 to +101
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a trailing comma on line 101 after the EOT heredoc closure. This comma appears to be unnecessary and could potentially cause parsing issues. The comma should be removed, and the 'returns' parameter should follow directly after the 'unless' parameter without an intervening comma.

Suggested change
EOT
,
EOT,

Copilot uses AI. Check for mistakes.
returns => [0],
tries => 3,
try_sleep=> 10,
timeout => 1200,
}
43 changes: 21 additions & 22 deletions spec/acceptance/sqlserver_role_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,6 @@
require 'securerandom'
require 'erb'

hostname = Helper.instance.run_shell('hostname').stdout.upcase.strip

# database name
db_name = "DB#{SecureRandom.hex(4)}".upcase
LOGIN1 = "Login1_#{SecureRandom.hex(2)}".freeze
LOGIN2 = "Login2_#{SecureRandom.hex(2)}".freeze
LOGIN3 = "Login3_#{SecureRandom.hex(2)}".freeze
Expand Down Expand Up @@ -47,8 +43,11 @@ def ensure_sqlserver_logins_users(db_name)

context 'Start testing sqlserver::role' do
before(:all) do
# Initialize hostname and db_name once per context
@hostname = Helper.instance.run_shell('hostname').stdout.upcase.strip
@db_name = "DB#{SecureRandom.hex(4)}".upcase
# Create database users
ensure_sqlserver_logins_users(db_name)
ensure_sqlserver_logins_users(@db_name)
end

before(:each) do
Expand Down Expand Up @@ -76,7 +75,7 @@ def ensure_sqlserver_logins_users(db_name)
admin_pass => 'Pupp3t1@',
}
sqlserver::user{'#{USER1}':
database => '#{db_name}',
database => '#{@db_name}',
ensure => 'absent',
}
MANIFEST
Expand All @@ -100,25 +99,25 @@ def ensure_sqlserver_logins_users(db_name)
apply_manifest(pp, catch_failures: true)

# validate that the database-specific role '#{@role}' is successfully created with specified permissions':
query = "USE #{db_name};
query = "USE #{@db_name};
SELECT spr.principal_id, spr.name,
spe.state_desc, spe.permission_name
FROM sys.server_principals AS spr
JOIN sys.server_permissions AS spe
ON spe.grantee_principal_id = spr.principal_id
WHERE spr.name = '#{@role}';"

run_sql_query(query:, server: hostname, expected_row_count: 2)
run_sql_query(query:, server: @hostname, expected_row_count: 2)

# validate that the database-specific role '#{@role}' has correct authorization #{LOGIN1}
query = "USE #{db_name};
query = "USE #{@db_name};
SELECT spr.name, sl.name
FROM sys.server_principals AS spr
JOIN sys.sql_logins AS sl
ON spr.owning_principal_id = sl.principal_id
WHERE sl.name = '#{LOGIN1}';"

run_sql_query(query:, server: hostname, expected_row_count: 1)
run_sql_query(query:, server: @hostname, expected_row_count: 1)
end

it "Create database-specific role #{@role}" do
Expand All @@ -130,23 +129,23 @@ def ensure_sqlserver_logins_users(db_name)
sqlserver::role{'DatabaseRole':
ensure => 'present',
role => '#{@role}',
database => '#{db_name}',
database => '#{@db_name}',
permissions => {'GRANT' => ['SELECT', 'INSERT', 'UPDATE', 'DELETE', 'CONTROL', 'ALTER']},
type => 'DATABASE',
}
MANIFEST
apply_manifest(pp, catch_failures: true)

# validate that the database-specific role '#{@role}' is successfully created with specified permissions':
query = "USE #{db_name};
query = "USE #{@db_name};
SELECT pr.principal_id, pr.name, pr.type_desc,
pr.authentication_type_desc, pe.state_desc, pe.permission_name
FROM sys.database_principals AS pr
JOIN sys.database_permissions AS pe
ON pe.grantee_principal_id = pr.principal_id
WHERE pr.name = '#{@role}';"

run_sql_query(query:, server: hostname, expected_row_count: 6)
run_sql_query(query:, server: @hostname, expected_row_count: 6)
end

it 'Create a database-specific role with the same name on two databases' do
Expand All @@ -158,7 +157,7 @@ def ensure_sqlserver_logins_users(db_name)
sqlserver::role{'DatabaseRole_1':
ensure => 'present',
role => '#{@role}',
database => '#{db_name}',
database => '#{@db_name}',
permissions => {'GRANT' => ['SELECT', 'INSERT', 'UPDATE', 'DELETE', 'CONTROL', 'ALTER']},
type => 'DATABASE',
}
Expand All @@ -180,11 +179,11 @@ def ensure_sqlserver_logins_users(db_name)
FROM sys.database_principals AS pr
JOIN sys.database_permissions AS pe
ON pe.grantee_principal_id = pr.principal_id
JOIN #{db_name}.sys.database_principals as dbpr
JOIN #{@db_name}.sys.database_principals as dbpr
on pr.name = dbpr.name
WHERE pr.name = '#{@role}';"

run_sql_query(query:, server: hostname, expected_row_count: 6)
run_sql_query(query:, server: @hostname, expected_row_count: 6)
end

it "Create server role #{@role} with optional members and optional members-purge" do
Expand All @@ -205,18 +204,18 @@ def ensure_sqlserver_logins_users(db_name)
apply_manifest(pp, catch_failures: true)

# validate that the server role '#{@role}' is successfully created with specified permissions':
query = "USE #{db_name};
query = "USE #{@db_name};
SELECT spr.principal_id AS ID, spr.name AS Server_Role,
spe.state_desc, spe.permission_name
FROM sys.server_principals AS spr
JOIN sys.server_permissions AS spe
ON spe.grantee_principal_id = spr.principal_id
WHERE spr.name = '#{@role}';"

run_sql_query(query:, server: hostname, expected_row_count: 2)
run_sql_query(query:, server: @hostname, expected_row_count: 2)

# validate that the t server role '#{@role}' has correct members (Login1, 2, 3)
query = "USE #{db_name};
query = "USE #{@db_name};
SELECT spr.principal_id AS ID, spr.name AS ServerRole
FROM sys.server_principals AS spr
JOIN sys.server_role_members m
Expand All @@ -226,7 +225,7 @@ def ensure_sqlserver_logins_users(db_name)
OR spr.name = '#{LOGIN3}'
OR spr.name = 'LOGIN4';"

run_sql_query(query:, server: hostname, expected_row_count: 3)
run_sql_query(query:, server: @hostname, expected_row_count: 3)

puts "Create server role #{@role} with optional members_purge:"
pp = <<-MANIFEST
Expand All @@ -247,7 +246,7 @@ def ensure_sqlserver_logins_users(db_name)
apply_manifest(pp, catch_failures: true)

# validate that the t server role '#{@role}' has correct members (only Login3)
query = "USE #{db_name};
query = "USE #{@db_name};
SELECT spr.principal_id AS ID, spr.name AS ServerRole
FROM sys.server_principals AS spr
JOIN sys.server_role_members m
Expand All @@ -256,7 +255,7 @@ def ensure_sqlserver_logins_users(db_name)
OR spr.name = '#{LOGIN2}'
OR spr.name = '#{LOGIN3}';"

run_sql_query(query:, server: hostname, expected_row_count: 1)
run_sql_query(query:, server: @hostname, expected_row_count: 1)
end
end
end
Loading
Loading