Skip to content

Commit

Permalink
Endpoint set-up, feature switch ok
Browse files Browse the repository at this point in the history
  • Loading branch information
fanf committed Jun 13, 2022
1 parent 3394a49 commit 2eb4fed
Show file tree
Hide file tree
Showing 7 changed files with 242 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ object ZipUtils {
/**
* Create the seq of zippable from a directory or file.
* If it's a directory, all children are added recursively,
* and there name are relative to the root.
* and their names are relative to the root.
* For a file, only its basename is added.
*
* The returned Zippable are ordered from root to children (deep first),
Expand All @@ -127,6 +127,8 @@ object ZipUtils {
* root/dir-b/file-a
* root/dir-b/file-b
* etc
*
* root name itself is not used.
*/
def toZippable(file: File): Seq[Zippable] = {
if (file.getParent == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,13 @@ trait ReadConfigService {
*/
def rudder_featureSwitch_directiveScriptEngine(): IOResult[FeatureSwitch]


/**
* Should we activate import/export archive API
*/
def rudder_featureSwitch_archiveApi(): IOResult[FeatureSwitch]


/**
* Default value for node properties after acceptation:
* - policy mode
Expand Down Expand Up @@ -324,6 +331,11 @@ trait UpdateConfigService {
*/
def set_rudder_featureSwitch_directiveScriptEngine(status: FeatureSwitch): IOResult[Unit]

/*
* Should we enable import/export archive API ?
*/
def set_rudder_featureSwitch_archiveApi(status: FeatureSwitch): IOResult[Unit]

/**
* Set the compliance mode
*/
Expand Down Expand Up @@ -427,6 +439,7 @@ class GenericConfigService(
rudder.policy.mode.name=${Enforce.name}
rudder.policy.mode.overridable=true
rudder.featureSwitch.directiveScriptEngine=enabled
rudder.featureSwitch.archiveApi=disabled
rudder.node.onaccept.default.state=enabled
rudder.node.onaccept.default.policyMode=default
rudder.compliance.unexpectedReportUnboundedVarValues=true
Expand Down Expand Up @@ -711,6 +724,12 @@ class GenericConfigService(
def rudder_featureSwitch_directiveScriptEngine(): IOResult[FeatureSwitch] = get("rudder_featureSwitch_directiveScriptEngine")
def set_rudder_featureSwitch_directiveScriptEngine(status: FeatureSwitch): IOResult[Unit] = save("rudder_featureSwitch_directiveScriptEngine", status)

/**
* Should we enable import/export archive API?
*/
def rudder_featureSwitch_archiveApi(): IOResult[FeatureSwitch] = get("rudder_featureSwitch_archiveApi")
def set_rudder_featureSwitch_archiveApi(status: FeatureSwitch): IOResult[Unit] = save("rudder_featureSwitch_archiveApi", status)

/**
* Default value for node properties after acceptation:
* - policy mode
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -873,6 +873,25 @@ object UserApi extends ApiModuleProvider[UserApi] {
def endpoints = ca.mrvisser.sealerate.values[UserApi].toList.sortBy( _.z )
}

/*
* An API for import & export of archives of objects with their dependencies
*/
sealed trait ArchiveApi extends EndpointSchema with GeneralApi with SortIndex {
override def dataContainer: Option[String] = None
}
object ArchiveApi extends ApiModuleProvider[ArchiveApi] {
final case object ExportSimple extends ArchiveApi with ZeroParam with StartsAtVersion15 with SortIndex {val z = implicitly[Line].value
val description = "Export the list of objects with their dependencies"
val (action, path) = GET / "archive" / "export"
}
final case object Import extends ArchiveApi with ZeroParam with StartsAtVersion15 with SortIndex {val z = implicitly[Line].value
val description = "Import an archive"
val (action, path) = POST / "archive" / "import"
}

def endpoints = ca.mrvisser.sealerate.values[ArchiveApi].toList.sortBy( _.z )
}

/*
* All API.
*/
Expand All @@ -888,6 +907,7 @@ object AllApi {
TechniqueApi.endpoints :::
RuleApi.endpoints :::
InventoryApi.endpoints :::
ArchiveApi.endpoints :::
InfoApi.endpoints :::
// UserApi is not declared here, it will be contributed by plugin
Nil
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ import com.normation.rudder.rest.lift.DefaultParams

/*
* This class deals with everything serialisation related for API.
* Change things with care! Everything must be versionned!
* Change things with care! Everything must be versioned!
* Even changing a field name can lead to an API incompatible change and
* so will need a new API version number (and be sure that old behavior is kept
* for previous versions).
Expand All @@ -58,7 +58,7 @@ import com.normation.rudder.rest.lift.DefaultParams
/*
* Rudder standard response.
* We normalize response format to look like what is detailed here: https://docs.rudder.io/api/v/13/#section/Introduction/Response-format
* Data are always name-spaced, so that theorically an answer can mixe several type of data. For example, for node details:
* Data are always name-spaced, so that theoretically an answer can mixe several type of data. For example, for node details:
* "data": { "nodes": [ ... list of nodes ... ] }
* And for globalCompliance:
* "data": { "globalCompliance": { ... } }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
/*
*************************************************************************************
* Copyright 2022 Normation SAS
*************************************************************************************
*
* This file is part of Rudder.
*
* Rudder is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* In accordance with the terms of section 7 (7. Additional Terms.) of
* the GNU General Public License version 3, the copyright holders add
* the following Additional permissions:
* Notwithstanding to the terms of section 5 (5. Conveying Modified Source
* Versions) and 6 (6. Conveying Non-Source Forms.) of the GNU General
* Public License version 3, when you create a Related Module, this
* Related Module is not considered as a part of the work and may be
* distributed under the license agreement of your choice.
* A "Related Module" means a set of sources files including their
* documentation that, without modification of the Source Code, enables
* supplementary functions or services in addition to those offered by
* the Software.
*
* Rudder is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Rudder. If not, see <http://www.gnu.org/licenses/>.
*
*************************************************************************************
*/

package com.normation.rudder.rest.lift

import com.normation.appconfig.ReadConfigService
import com.normation.appconfig.UpdateConfigService
import com.normation.rudder.api.ApiVersion
import com.normation.rudder.domain.appconfig.FeatureSwitch
import com.normation.rudder.domain.logger.ApplicationLogger
import com.normation.rudder.git.ZipUtils
import com.normation.rudder.git.ZipUtils.Zippable
import com.normation.rudder.rest.ApiPath
import com.normation.rudder.rest.AuthzToken
import com.normation.rudder.rest.RudderJsonResponse
import com.normation.rudder.rest.RudderJsonResponse.ResponseSchema
import com.normation.rudder.rest.implicits._
import com.normation.rudder.rest.lift.DummyImportAnswer._
import com.normation.rudder.rest.{ArchiveApi => API}

import net.liftweb.http.LiftResponse
import net.liftweb.http.OutputStreamResponse
import net.liftweb.http.Req

import java.io.ByteArrayInputStream
import java.io.InputStream
import java.io.OutputStream
import java.nio.charset.StandardCharsets

import zio._
import zio.syntax._
import com.normation.errors._
import com.normation.zio._

/*
* Machinery to enable/disable the API given the value of the feature switch in config service.
* If disabled, always return an error with the info about how to enable it.
*/
final case class FeatureSwitch0[A <: LiftApiModule0](enable: A, disable: A)(configService: ReadConfigService) extends LiftApiModule0 {
override val schema = enable.schema
override def process0(version: ApiVersion, path: ApiPath, req: Req, params: DefaultParams, authzToken: AuthzToken): LiftResponse = {
configService.rudder_featureSwitch_archiveApi().either.runNow match {
case Left(err) =>
ApplicationLogger.error(err.fullMsg)
RudderJsonResponse.internalError(ResponseSchema.fromSchema(schema), err.fullMsg)(params.prettify).toResponse
case Right(FeatureSwitch.Disabled) =>
disable.process0(version, path, req, params, authzToken)
case Right(FeatureSwitch.Enabled) =>
enable.process0(version, path, req, params, authzToken)
}
}
}

class ArchiveApi(
configService: ReadConfigService with UpdateConfigService
) extends LiftApiModuleProvider[API] {

def schemas = API

def getLiftEndpoints(): List[LiftApiModule] = {
API.endpoints.map(e => e match {
case API.Import => FeatureSwitch0(Import, ImportDisabled)(configService)
case API.ExportSimple => FeatureSwitch0(ExportSimple, ExportSimpleDisabled)(configService)
})
}

/*
* Default answer to use when the feature is disabled
*/
trait ApiDisabled extends LiftApiModule0 {
override def process0(version: ApiVersion, path: ApiPath, req: Req, params: DefaultParams, authzToken: AuthzToken): LiftResponse = {
RudderJsonResponse.internalError(
ResponseSchema.fromSchema(schema)
, "This API is disabled. You enable it with the setting `rudder.featureSwitch.archiveApi` in setting API set to `enabled`"
)(params.prettify).toResponse
}
}

object ExportSimpleDisabled extends LiftApiModule0 with ApiDisabled { val schema = API.ExportSimple }

/*
* This API does not returns a standard JSON response, it returns a ZIP archive.
*/
object ExportSimple extends LiftApiModule0 {
val schema = API.ExportSimple
def process0(version: ApiVersion, path: ApiPath, req: Req, params: DefaultParams, authzToken: AuthzToken): LiftResponse = {
def getContent(use: InputStream => IOResult[Any]): IOResult[Any] = {
ZIO.bracket(IOResult.effect(new ByteArrayInputStream("Hello world!".getBytes(StandardCharsets.UTF_8))))(is => effectUioUnit(is.close)) { is =>
use(is)
}
}
val name = "archive"
val archive = Chunk(
Zippable(name, None)
, Zippable(s"${name}/placeholder", Some(getContent _))
, Zippable(s"${name}/placeholder2", Some(getContent _))
)

//do zip
val send = (os: OutputStream) => ZipUtils.zip(os, archive).runNow

val headers = List(
("Pragma", "public")
, ("Expires", "0")
, ("Cache-Control", "must-revalidate, post-check=0, pre-check=0")
, ("Cache-Control", "public")
, ("Content-Description", "File Transfer")
, ("Content-type", "application/octet-stream")
, ("Content-Disposition", s"""attachment; filename="${name}.zip"""")
, ("Content-Transfer-Encoding", "binary")
)

new OutputStreamResponse(send, -1, headers, Nil, 200)

}
}

object ImportDisabled extends LiftApiModule0 with ApiDisabled { override val schema = API.Import }

object Import extends LiftApiModule0 {
val schema = API.Import
def process0(version: ApiVersion, path: ApiPath, req: Req, params: DefaultParams, authzToken: AuthzToken): LiftResponse = {
val res: IOResult[JRArchiveImported] = JRArchiveImported(true).succeed
res.toLiftResponseOne(params, schema, _ => None)
}
}
}

/*
* A dummy object waiting for implementation for import
*/
object DummyImportAnswer {

import zio.json._

case class JRArchiveImported(success: Boolean)

implicit lazy val encodeJRArchiveImported: JsonEncoder[JRArchiveImported] = DeriveJsonEncoder.gen

}
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ class SettingsApi(
RestComputeDynGroupMaxParallelism ::
RestSetupDone ::
RestRuddercTargets ::
ArchiveApiFeatureSwitch ::
Nil

val allSettings_v12 = RestReportProtocolDefault :: allSettings_v10
Expand Down Expand Up @@ -598,7 +599,7 @@ final case object RestSendMetrics extends RestSetting[Option[SendMetrics]] {
def set = configService.set_send_server_metrics _
}

final case object RestJSEngine extends RestSetting[FeatureSwitch] {
final case object RestJSEngine extends RestSetting[FeatureSwitch] {
val startPolicyGeneration = true
def toJson(value : FeatureSwitch) : JValue = value.name
def parseJson(json: JValue) = {
Expand All @@ -615,7 +616,24 @@ final case object RestJSEngine extends RestSetting[FeatureSwitch] {
def set = (value : FeatureSwitch, _, _) => configService.set_rudder_featureSwitch_directiveScriptEngine(value)
}

final case object RestOnAcceptPolicyMode extends RestSetting[Option[PolicyMode]] {
final case object ArchiveApiFeatureSwitch extends RestSetting[FeatureSwitch] {
val startPolicyGeneration = false
def toJson(value : FeatureSwitch) : JValue = value.name
def parseJson(json: JValue) = {
json match {
case JString(value) => FeatureSwitch.parse(value)
case x => Failure("Invalid value "+x)
}
}
def parseParam(param : String) = {
FeatureSwitch.parse(param)
}
val key = "rudder_featureSwitch_archiveApi"
def get = configService.rudder_featureSwitch_archiveApi()
def set = (value : FeatureSwitch, _, _) => configService.set_rudder_featureSwitch_archiveApi(value)
}

final case object RestOnAcceptPolicyMode extends RestSetting[Option[PolicyMode]] {
val startPolicyGeneration = false
def parseParam(value: String): Box[Option[PolicyMode]] = {
Full(PolicyMode.allModes.find( _.name == value))
Expand All @@ -631,7 +649,8 @@ final case object RestOnAcceptPolicyMode extends RestSetting[Option[PolicyMode]]
def get = configService.rudder_node_onaccept_default_policy_mode()
def set = (value : Option[PolicyMode], _, _) => configService.set_rudder_node_onaccept_default_policy_mode(value)
}
final case object RestOnAcceptNodeState extends RestSetting[NodeState] {

final case object RestOnAcceptNodeState extends RestSetting[NodeState] {
val startPolicyGeneration = false
def parseParam(value: String): Box[NodeState] = {
Full(NodeState.values.find( _.name == value).getOrElse(NodeState.Enabled))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1330,6 +1330,7 @@ object RudderConfig extends Loggable {
ApiVersion(12 , true) :: // rudder 6.0, 6.1
ApiVersion(13 , true) :: // rudder 6.2
ApiVersion(14 , false) :: // rudder 7.0
ApiVersion(15 , false) :: // rudder 7.1
Nil

val jsonPluginDefinition = new ReadPluginPackageInfo("/var/rudder/packages/index.json")
Expand All @@ -1356,6 +1357,7 @@ object RudderConfig extends Loggable {
, new PluginApi(restExtractorService, pluginSettingsService)
, new RecentChangesAPI(recentChangesService, restExtractorService)
, new RulesInternalApi(restExtractorService, ruleInternalApiService)
, new ArchiveApi(configService)
// info api must be resolved latter, because else it misses plugin apis !
)

Expand Down

0 comments on commit 2eb4fed

Please sign in to comment.